重新回答:何謂非同步(異步)?
這大概是前端工程師最常見的面試題目了:
在 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();
大概就是這樣囉。
很多時候,要回歸初心、莫忘初衷。