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 誕生於瀏覽器內部,最初的主要工作是響應用戶操作,如onClickonMouseOveronChangeonSubmit等等。它如何在同步的編程模型下實現這一點?

答案就在於它的環境。瀏覽器提供了一套可以處理這類功能的 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)。