重新回答:何謂非同步(異步)?

這大概是前端工程師最常見的面試題目了:

在 JavaScript 裡面,非同步(異步)是什麼?

很久以前我寫過「Callback Hell:一點迷宮」。不過那只是講 Callback Hell ──非同步機制會出現的反模式──而已。現在,我決定以 Callback Hell 為基礎,再稍微回答多一點。

英文的 asynchronous 在中文一般稱為「非同步」或「異步」。以下將把 asynchronous 翻成非同步

所以究竟何謂非同步?他幹嘛的?

由於 JavaScript 設計上是單線程(Single Threaded):瀏覽器在一般狀況下,一次只能做一件事。因此,如果程式需要作一些不會馬上完成的工作──比方說讀取檔案──的話,整個程式就會停在那個工作,用戶因此無法完成其他事情。這樣的話前端體驗會很糟。因此,如果要在 JavaScript 處理這些不會馬上完成的工作,就需要設計新的機制來處理。這就是非同步。

在 JavaScript 處理非同步的最經典例子,要算是 AJAX (Asynchronous JavaScript and XML) 技術了:它主要會向伺服器請求一筆小資料──也就是 Web API──然後將資料畫在網頁上。實做例子就像這樣:

const main = function () {
    const api = fetch("https://example.com/api");
    // 繼續……
};
main();

接下來有關非同步的例子都會在 AJAX 方面打轉。在此將使用原生的 fetch 完成 AJAX 請求。

怎麼使用非同步呢?

因為 AJAX 不會馬上回應,所以直接用肯定不會過。一般而言是回呼(callback)。

const main = function () {
    const api = fetch("https://example.com/api").then( function (r) { return r.json(); } );
    api.then( function (response) {
        // 回呼函式內部. Inside the callback function...
    });
};
main();

api.then 有多了個函式,看到了嗎?因為我們需要 callback 處理完成非同步後的動作,所以我們在裡面寫了個 callback function。

這就是我們處理非同步的做法──問題是如果有好幾個 AJAX 要處理呢?比方說,我必須要先請求 /api、再處理 /api2、再處理 /api3……呢?

// 噢,我們還換成箭頭函式了。箭頭函式的英文是 arrow function。
const main = () => {
    fetch("https://example.com/api").then( (r) => r.json() ).then( (res1) => {
        fetch("https://example.com/api2").then( (r) => r.json() ).then( (res2) => {
            fetch("https://example.com/api3").then( (r) => r.json() ).then( (res3) => {
                fetch("https://example.com/api4").then( (r) => r.json() ).then( (res4) => {
                    // 繼續處理……
                });
            });
        });
    });
};
main();

很明顯這種寫法是不能接受的,因為它會讓維護非常困難。

但這就是 Callback Hell。

如何解決?

最簡單、最基礎的作法,就是把函式拆成模塊。從一開始寫文章到現在,這答案從來沒變。

const main = () => {
    const get_json = (r) => r.json();
    const show_error = (error) => console.error(error);
    const get_api_1st = () => fetch("https://example.com/api").then( get_json ).then( get_api_2nd ).catch( show_error );
    const get_api_2nd = (res1) => fetch("https://example.com/api2").then( get_json ).then( (res2) => {
        get_api_3rd({ res1, res2 });
    }).catch( show_error );
    const get_api_3rd = (old_res) => fetch("https://example.com/api3").then( get_json ).then( (res3) => {
        get_api_4th({ ...old_res, res3 });
    }).catch( show_error );
    const get_api_4th = (old_res) => fetch("https://example.com/api4").then( get_json ).then( (res4) => {
        do_action({ ...old_res, res4 });
    }).catch( show_error );
    const do_action = (old_res = { res1 = "", res2 = "", res3 = "", res4 = "" }) => {
        // 繼續處理……
    };
    get_api_1st();
};
main();

如果還不夠潮的話,可以用 Promise

const main = async () => {
    const get_json = (r) => r.json();
    const show_error = (error) => console.error(error);
    const get_api_1st = () => fetch("https://example.com/api").then( get_json );
    const get_api_2nd = (res1) => fetch("https://example.com/api2").then( get_json ).then((res2) => ({ res1, res2 }));
    const get_api_3rd = ({ res1, res2 }) => fetch("https://example.com/api3").then( get_json ).then((res3) => ({ res1, res2, res3 }));
    const get_api_4th = ({ res1, res2, res3 }) => fetch("https://example.com/api4").then( get_json ).then((res4) => ({ res1, res2, res3, res4 }));
    const do_action = ({ res1, res2, res3, res4 }) => {
        // 繼續處理……
    };
    get_api_1st()
        .then(get_api_2nd)
        .then(get_api_3rd)
        .then(get_api_4th)
        .then(do_action)
        .catch( show_error );
};
main();

甚至是 async/await

const main = async () => {
    const get_json = (r) => r.json();
    const show_error = (error) => console.error(error);
    const res1 = await fetch("https://example.com/api").then( get_json );
    const res2 = await fetch("https://example.com/api2").then( get_json );
    const res3 = await fetch("https://example.com/api3").then( get_json );
    const res4 = await fetch("https://example.com/api4").then( get_json );
    // 繼續處理……
    // res1, res2, res3, res4
};
main();

大概就是這樣囉。

很多時候,要回歸初心、莫忘初衷。

參考資料