JavaScript 非同步程式設計與回呼函式
程式語言中的非同步性
電腦設計上是非同步的。
非同步意味著事情可以獨立於主程式流程發生。
在目前的消費型電腦中,每個程式都會運行一個特定的時間段,然後停止執行,讓另一個程式繼續執行。這個過程非常快速,幾乎不可察覺。我們認為我們的電腦同時運行許多程式,但這只是一種幻覺(在多處理器機器上除外)。
程式內部使用中斷,這是發送給處理器以獲得系統注意的信號。
現在我們不深入討論內部機制,只需記住,程式通常是異步的,並在需要注意時暫停執行,允許電腦在此期間執行其他任務。當程式正在等待來自網路的響應時,它無法暫停處理器,直到請求完成。
通常,編程語言是同步的,一些提供了在語言或通過庫中管理異步性的方法。C、Java、C#、PHP、Go、Ruby、Swift 和 Python 預設都是同步的。其中一些通過使用線程來處理異步操作,生成一個新的進程。
JavaScript
JavaScript 預設是同步的,且是單線程的。這意味著代碼不能創建新的線程並且並行運行。
代碼行是按順序一個接一個執行的,例如
const a = 1;
const b = 2;
const c = a * b;
console.log(c);
doSomething();
但 JavaScript 誕生於瀏覽器內部,最初的主要工作是響應用戶操作,如onClick
、onMouseOver
、onChange
、onSubmit
等等。它如何在同步的編程模型下實現這一點?
答案就在於它的環境。瀏覽器提供了一套可以處理這類功能的 API。
更近期,Node.js 引入了非阻塞的 I/O 環境,將此概念擴展到文件訪問、網路調用等方面。
回呼函式
您無法預知使用者何時會點擊按鈕。因此,您需要為點擊事件定義一個事件處理器。這個事件處理器接受一個函式作為參數,當事件被觸發時該函式將被呼叫。
document.getElementById('button').addEventListener('click', () => {
// item clicked
});
這就是所謂的「回呼函式」。
回呼函式是一個簡單的函式,作為值傳遞給另一個函式,只有當事件發生時才會被執行。這是因為 JavaScript 具有一等公民函式,可以被指派給變數並傳遞給其他函式(稱為「高階函式」)。
在 window
物件上使用 load
事件監聽器將所有客戶端代碼包裹起來是很常見的,這樣在頁面準備就緒時只運行回呼函式。
window.addEventListener('load', () => {
// window loaded
// do what you want
});
回呼函式到處都在使用,不僅僅是在 DOM 事件中。
一個常見的例子是使用計時器。
setTimeout(() => {
// runs after 2 seconds
}, 2000);
XHR 請求也接受回呼函式,在此示例中,通過將函式指派給屬性,當特定事件發生時該函式將被呼叫(在本例中是請求狀態改變時)。
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
xhr.status === 200 ? console.log(xhr.responseText) : console.error('error');
}
};
xhr.open('GET', 'https://yoursite.com');
xhr.send();
處理回呼函式中的錯誤
如何處理回呼函式中的錯誤?一個非常常見的策略是使用 Node.js 採用的方式:回呼函式中的第一個參數是錯誤物件:錯誤優先的回呼函式。
如果沒有錯誤,該物件為 null
。如果有錯誤,它包含錯誤的描述和其他信息。
const fs = require('node:fs');
fs.readFile('/file.json', (err, data) => {
if (err) {
// handle error
console.log(err);
return;
}
// no errors, process data
console.log(data);
});
回呼函式的問題
對於簡單的情況,回呼函式是很棒的!
然而,每個回呼函式都增加了一層嵌套,當您有大量的回呼函式時,代碼很快就變得複雜了。
window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
// your code here
});
}, 2000);
});
});
這只是一個簡單的 4 層代碼,但我已經看到更多層次的嵌套,這並不好玩。
我們如何解決這個問題?
回呼函式的替代方案
從ES6開始,JavaScript引入了幾個不需要使用回呼函式的異步程式碼的特性:Promises(ES6)和Async/Await(ES2017)。