JavaScript 非同步程式設計與回撥
程式語言中的非同步性
計算機生來就是非同步的。
非同步意味著事情可以在主程式流程之外獨立發生。
在當前的消費級計算機中,每個程式都會執行一個特定的時間片,然後停止執行,讓另一個程式繼續執行。這個過程以極快的速度迴圈進行,以至於我們無法察覺。我們以為計算機能同時執行多個程式,但這其實是一種錯覺(多處理器機器除外)。
程式在內部使用*中斷*,這是一種向處理器發出的訊號,以獲得系統的關注。
我們現在不深入探討其內部原理,但請記住,程式是非同步的是正常的,它們會暫停執行直到需要關注,從而允許計算機在此期間執行其他任務。當一個程式在等待網路響應時,它不能一直佔用處理器直到請求完成。
通常,程式語言是同步的,有些語言透過自身或庫提供了管理非同步性的方式。C、Java、C#、PHP、Go、Ruby、Swift 和 Python 預設都是同步的。其中一些透過使用執行緒來處理非同步操作,即生成一個新程序。
JavaScript
JavaScript 預設是同步且單執行緒的。這意味著程式碼不能建立新執行緒並行執行。
程式碼行是序列執行的,一行接一行,例如:
const = 1;
const = 2;
const = * ;
.();
doSomething();
但 JavaScript 誕生於瀏覽器,它最初的主要工作是響應使用者操作,如 onClick、onMouseOver、onChange、onSubmit 等。它如何透過同步程式設計模型來做到這一點呢?
答案在於它的環境。瀏覽器透過提供一組可以處理這類功能的 API 來實現這一點。
最近,Node.js 引入了一個非阻塞 I/O 環境,將這一概念擴充套件到檔案訪問、網路呼叫等領域。
回撥
你無法預知使用者何時會點選一個按鈕。所以,你為點選事件定義一個事件處理程式。這個事件處理程式接受一個函式,該函式將在事件被觸發時呼叫:
.('button').('click', () => {
// item clicked
});
這就是所謂的回撥。
回撥是一個簡單的函式,它作為值傳遞給另一個函式,並且只在事件發生時才會被執行。我們能這樣做,是因為 JavaScript 擁有一等函式,函式可以被賦值給變數並傳遞給其他函式(稱為高階函式)。
通常的做法是將所有客戶端程式碼包裹在 window 物件的 load 事件監聽器中,它只在頁面準備好後才執行回撥函式:
.('load', () => {
// window loaded
// do what you want
});
回撥無處不在,不僅僅用於 DOM 事件。
一個常見的例子是使用定時器:
(() => {
// runs after 2 seconds
}, 2000);
XHR 請求也接受回撥,在這個例子中,透過給一個屬性賦值一個函式,當特定事件發生時(在這裡是請求狀態改變),該函式將被呼叫:
const = new ();
. = () => {
if (. === 4) {
if (. === 200) {
.(.);
} else {
.('error');
}
}
};
.('GET', 'https://yoursite.com');
.();
在回撥中處理錯誤
如何用回撥處理錯誤?一個非常常見的策略是使用 Node.js 所採用的方法:任何回撥函式的第一個引數都是錯誤物件,即**錯誤優先的回撥**。
如果沒有錯誤,該物件為 null。如果存在錯誤,它會包含錯誤的描述和其他資訊。
const = ('node:fs');
.('/file.json', (, ) => {
if () {
// handle error
.();
return;
}
// no errors, process data
.();
});
回撥的問題
對於簡單情況,回撥非常棒!
然而,每個回撥都會增加一層巢狀,當回撥很多時,程式碼很快就會變得複雜:
.('load', () => {
.('button').('click', () => {
(() => {
items.forEach( => {
// your code here
});
}, 2000);
});
});
這只是一個簡單的 4 層程式碼,但我見過更多層的巢狀,那可不是什麼好玩的事。
我們該如何解決這個問題?
回撥的替代方案
從 ES6 開始,JavaScript 引入了幾個有助於我們處理非同步程式碼且不涉及回撥的特性:Promise (ES6) 和 Async/Await (ES2017)。