深入理解JavaScript的事件迴圈、callback、promise和asyncawait

深入理解JavaScript的事件迴圈、callback、promise和async/await

現在Web應用程式對網頁的動態交換需要越來越高,因此越來越有必要進行密集的操作,例如發出外部網路請求以檢索API資料。要在 JavaScript 中處理這些操作,開發人員必須使用非同步程式設計技術。

由於 JavaScript 是一種具有同步執行模型的單執行緒程式語言,用於處理一個又一個操作,因此它一次只能處理一個語句。但是,從 API 請求資料等操作可能需要不確定的時間,具體取決於所請求的資料大小、網路連線速度和其他因素。如果以同步方式執行 API 呼叫,則在該操作完成之前,瀏覽器將無法處理任何使用者輸入,例如滾動或單擊按鈕。這稱為阻止。

為了防止阻塞行為,瀏覽器環境具有許多 JavaScript 可以訪問的非同步 Web API,這意味著它們可以與其他操作並行執行,而不是按順序執行。這很有用,因為它允許使用者在處理非同步操作時繼續正常使用瀏覽器。

作為 JavaScript 開發人員,您需要知道如何使用非同步 Web API 並處理這些操作的響應或錯誤。在本文中,您將瞭解事件迴圈、透過回撥處理非同步行為的原始方法、更新的 ECMAScript 2015 新增的承諾以及使用非同步/await 的現代實踐。

事件迴圈The Event Loop

下面開始介紹JavaScript如何透過事件迴圈Event Loop 實現非同步操作的。

// Define three example functionsfunction step_first() { console。log(1)}function step_second() { console。log(2)}function step_third() { console。log(3)}

執行後,將會得到如下順序輸出:

// Execute the functionsfirst()second()third()

也就是依次執行了step_first() \ step_second() \ step_third()

輸出是

123

下面以內建函式setTimeout 來演示非同步機制。

// Define three example functions, but one of them contains asynchronous codefunction step_first() { console。log(1)}function step_second() { setTimeout(() => { console。log(2) }, 0)}function step_third() { console。log(3)}

也是依次執行了step_first() \ step_second() \ step_third()

但輸出卻是:

132

無論將setTimeOut 的超時設定為0秒還是5分鐘都不會有什麼區別

- 非同步程式碼呼叫的console。log將在同步頂級函式之後執行。

發生這種情況是因為 JavaScript 主機環境(在本例中為瀏覽器)使用稱為事件迴圈Event Loop的概念來處理併發或並行事件。

棧Stack

棧或呼叫堆疊儲存當前正在執行的函式的狀態。如果您不熟悉堆疊的概念,則可以將其想象為具有“最後進先出”(LIFO) 屬性的陣列,這意味著您只能在堆疊的末尾新增或刪除專案。JavaScript 將在堆疊中運行當前幀(或特定環境中的函式呼叫),然後將其刪除並轉到下一個幀。

對於僅包含同步程式碼的示例,瀏覽器按以下順序處理執行:

新增step_first()到 棧stack, 執行step_first() 當輸出1 後, 從棧stack 刪除step_first()。

新增step_second() 到 棧stack, 執行step_second() 當輸出1 後, 從棧stack 刪除step_second() 。

新增step_third() 到 棧stack, 執行step_third() 當輸出1 後, 從棧stack 刪除step_third() 。

對於使用了setTimeout 的例子,執行順序如下:

新增step_first()到 棧stack, 執行step_first() 當輸出1 後, 從棧stack 刪除step_first()。。

新增step_second()到 棧stack, 執行step_second()。新增setTimeout() 到棧,執行 setTimeout()啟動一個定時器 timer 並將steTimeout 設定的非同步函式新增到佇列

queue

, 從棧刪除setTimeout()

從棧stack刪除step_second()。

新增step_third() 到 棧stack, 執行step_third() 當輸出1 後, 從棧stack 刪除step_third()。

事件迴圈檢查佇列 queue 找到非同步函式setTimeout(), 新增console。log(2)到棧 stack,然後執行console。log(2)。 輸出2 後,從棧刪除console。log(2)。

佇列queue

佇列(也稱為訊息佇列或任務佇列)是函式的等待區域。每當呼叫堆疊為空時,事件迴圈將從最舊的訊息開始檢查佇列中是否有任何等待的訊息。一旦找到一個,它就會將其新增到堆疊中,這將執行訊息中的函式。

回撥函式Callback Functions

在 setTimeout 示例中,具有超時的函式在主頂級執行上下文中的所有內容之後執行。但是,如果要確保其中一個函式(如step_thrid函式)在超時後執行,則必須使用非同步編碼方法。此處的超時可以表示包含資料的非同步 API 呼叫。您希望使用來自 API 呼叫的資料,但必須確保首先返回資料。

處理此問題的原始解決方案是使用回撥函式

Callback Functions

。回撥函式沒有特殊的語法;它們只是一個作為引數傳遞給另一個函式的函式。將另一個函式作為引數的函式稱為高階函式。根據此定義,如果將任何函式作為引數傳遞,則可以成為回撥函式。回撥本質上不是非同步的,但可用於非同步目的。

// A functionfunction fn() { console。log(‘Just a function’)}// A function that takes another function as an argumentfunction higherOrderFunction(callback) { // When you call a function that is passed as an argument, it is referred to as a callback callback()}// Passing a functionhigherOrderFunction(fn)

在此程式碼中,您將定義一個函式 fn,定義一個將函式回撥作為引數的函式更高階函式,並將 fn 作為回撥傳遞給更高階函式。

執行此程式碼將提供以下內容:

Just a function

完整例子如下

// Define three functionsfunction first() { console。log(1)}function second(callback) { setTimeout(() => { console。log(2) // Execute the callback function callback() }, 0)}function third() { console。log(3)}

巢狀回撥和末日金字塔Nested Callbacks and the Pyramid of Doom

回撥函式是確保延遲執行函式直到另一個函式完成並返回資料的有效方法。但是,由於回撥的巢狀性質,如果您有大量相互依賴的連續非同步請求,則程式碼最終可能會變得混亂。對於早期的JavaScript開發人員來說,這是一個很大的挫折,因此包含巢狀回撥的程式碼通常被稱為“厄運金字塔”或“回撥地獄”。

如:

function pyramidOfDoom() { setTimeout(() => { console。log(1) setTimeout(() => { console。log(2) setTimeout(() => { console。log(3) }, 500) }, 2000) }, 1000)}

在實踐中,使用現實世界的非同步程式碼,這可能會變得更加複雜。您很可能需要在非同步程式碼中執行錯誤處理,然後將每個響應中的一些資料傳遞到下一個請求。使用回撥執行此操作會使程式碼難以閱讀和維護。

下面是一個更現實的“厄運金字塔”的可執行示例,您可以玩弄它:

// Example asynchronous functionfunction asynchronousRequest(args, callback) { // Throw an error if no arguments are passed if (!args) { return callback(new Error(‘Whoa! Something went wrong。’)) } else { return setTimeout( // Just adding in a random number so it seems like the contrived asynchronous function // returned different data () => callback(null, { body: args + ‘ ’ + Math。floor(Math。random() * 10) }), 500 ) }}// Nested asynchronous requestsfunction callbackHell() { asynchronousRequest(‘First’, function first(error, response) { if (error) { console。log(error) return } console。log(response。body) asynchronousRequest(‘Second’, function second(error, response) { if (error) { console。log(error) return } console。log(response。body) asynchronousRequest(null, function third(error, response) { if (error) { console。log(error) return } console。log(response。body) }) }) })}// ExecutecallbackHell()

輸出:

First 9Second 3Error: Whoa! Something went wrong。 at asynchronousRequest (:4:21) at second (:29:7) at :9:13

承諾Promises

承諾Promises表示非同步函式的完成。它是一個將來可能返回值的物件。它實現了與回撥函式相同的基本目標,但具有許多其他功能和更具可讀性的語法。作為 JavaScript 開發人員,您可能會花費比建立承諾更耗時,因為通常非同步 Web API 會返回承諾供開發人員使用。本教程將向您展示如何同時執行這兩項操作。

建立一個承諾Creating a Promise

// Initialize a promiseconst promise = new Promise((resolve, reject) => {})

在瀏覽器的console 可以看到輸出:

__proto__: Promise[[PromiseStatus]]: “pending”[[PromiseValue]]: undefined

增加resolve 的內容

const promise = new Promise((resolve, reject) => { resolve(‘We did it!’)})

瀏覽器console 可以看到輸出:

__proto__: Promise[[PromiseStatus]]: “fulfilled”[[PromiseValue]]: “We did it!”

promise 有三種狀態: pending, fulfilled, and rejected。

Pending

- 初始狀態

Fulfilled

- 成功狀態, promise has resolved

Rejected

- 失敗操作, promise has rejected

使用承諾Consuming a Promise

上一節中的承諾已透過值實現,但您還希望能夠訪問該值。承諾有一個呼叫的方法,該方法將在承諾在程式碼中解析後執行。然後將返回承諾的值作為引數。

以下是返回並記錄示例承諾值的方式:

promise。then((response) => { console。log(response)})

We did it!

到目前為止,您建立的示例不涉及非同步 Web API,它僅解釋瞭如何建立、解析和使用本機 JavaScript 承諾。使用 set超時,您可以測試非同步請求。

const promise = new Promise((resolve, reject) => { setTimeout(() => resolve(‘Resolving an asynchronous request!’), 2000)})// Log the resultpromise。then((response) => { console。log(response)})

Resolving an asynchronous request!

then 可以鏈式呼叫

// Chain a promisepromise 。then((firstResponse) => { // Return a new value for the next then return firstResponse + ‘ And chaining!’ }) 。then((secondResponse) => { console。log(secondResponse) })

Resolving an asynchronous request! And chaining!

錯誤處理Error Handling

function getUsers(onSuccess) { return new Promise((resolve, reject) => { setTimeout(() => { // Handle resolve and reject in the asynchronous API if (onSuccess) { resolve([ { id: 1, name: ‘Jerry’ }, { id: 2, name: ‘Elaine’ }, { id: 3, name: ‘George’ }, ]) } else { reject(‘Failed to fetch data!’) } }, 1000) })}

為了處理該錯誤,您將使用 catch 例項方法。這將為您提供一個失敗回撥,其中錯誤作為引數。

// Run the getUsers function with the false flag to trigger an errorgetUsers(false) 。then((response) => { console。log(response) }) 。catch((error) => { console。error(error) })

Failed to fetch data!

使用 Fetch 和 Promises

返回承諾的最有用和最常用的 Web API 之一是 Fetch API,它允許您透過網路發出非同步資源請求。獲取是一個由兩部分組成的過程,因此需要連結。此示例演示如何透過 API 獲取使用者的資料,同時處理任何潛在的錯誤:

// Fetch a user from the prism3d。cn APIfetch(‘https://api。prism3d。cn/users/octocat’) 。then((response) => { return response。json() }) 。then((data) => { console。log(data) }) 。catch((error) => { console。error(error) })

login: “octocat”,id: 574232,avatar_url: “https://avatars。prism3d。cn/u/574232”blog: “https://blog。prism3d。cn”company: “@prism3d”followers: 1203。。。

非同步函式使用 async/await

非同步函式允許您以顯示為同步的方式處理非同步程式碼。非同步函式仍然在引擎蓋下使用承諾,但具有更傳統的JavaScript語法。在本節中,您將嘗試此語法的示例。

您可以透過在函式之前新增非同步關鍵字來建立非同步函式:

// Create an async functionasync function getUser() { return {}}

與如下功能相同,但是更為簡潔容易理解

getUser()。then((response) => console。log(response))

完整樣例如下:

// Handling success and errors with async/awaitasync function getUser() { try { // Handle success in try const response = await fetch(‘https://api。prism3d。cn/users/octocat’) const data = await response。json() console。log(data) } catch (error) { // Handle error in catch console。error(error) }}