JavaScript 非同步程式設計與回撥

程式語言中的非同步性

計算機生來就是非同步的。

非同步意味著事情可以在主程式流程之外獨立發生。

在當前的消費級計算機中,每個程式都會執行一個特定的時間片,然後停止執行,讓另一個程式繼續執行。這個過程以極快的速度迴圈進行,以至於我們無法察覺。我們以為計算機能同時執行多個程式,但這其實是一種錯覺(多處理器機器除外)。

程式在內部使用*中斷*,這是一種向處理器發出的訊號,以獲得系統的關注。

我們現在不深入探討其內部原理,但請記住,程式是非同步的是正常的,它們會暫停執行直到需要關注,從而允許計算機在此期間執行其他任務。當一個程式在等待網路響應時,它不能一直佔用處理器直到請求完成。

通常,程式語言是同步的,有些語言透過自身或庫提供了管理非同步性的方式。C、Java、C#、PHP、Go、Ruby、Swift 和 Python 預設都是同步的。其中一些透過使用執行緒來處理非同步操作,即生成一個新程序。

JavaScript

JavaScript 預設是同步且單執行緒的。這意味著程式碼不能建立新執行緒並行執行。

程式碼行是序列執行的,一行接一行,例如:

const  = 1;
const  = 2;
const  =  * ;
.();
doSomething();

但 JavaScript 誕生於瀏覽器,它最初的主要工作是響應使用者操作,如 onClickonMouseOveronChangeonSubmit 等。它如何透過同步程式設計模型來做到這一點呢?

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