異步 JavaScript:從回調地獄到異步和等待

已發表: 2022-03-11

編寫成功的 Web 應用程序的關鍵之一是能夠在每個頁面上進行數十次 AJAX 調用。

這是一個典型的異步編程挑戰,您如何選擇處理異步調用將在很大程度上決定您的應用程序的成敗,進而可能影響您的整個啟動。

長期以來,在 JavaScript 中同步異步任務是一個嚴重的問題。

這一挑戰對使用 Node.js 的後端開發人員的影響與對使用任何 JavaScript 框架的前端開發人員的影響一樣大。 異步編程是我們日常工作的一部分,但挑戰往往被輕視,沒有在正確的時間考慮。

異步 JavaScript 簡史

第一個也是最直接的解決方案是以作為回調的嵌套函數的形式出現的。 這個解決方案導致了一種叫做回調地獄的東西,太多的應用程序仍然覺得它很痛苦。

然後,我們得到了Promises 。 這種模式使代碼更易於閱讀,但與不要重複自己 (DRY) 原則相去甚遠。 在很多情況下,您必須重複相同的代碼才能正確管理應用程序的流程。 最新添加的 async/await 語句形式終於使 JavaScript 中的異步代碼與任何其他代碼一樣易於讀寫。

讓我們看一下每個解決方案的示例,並反思 JavaScript 中異步編程的演變。

為此,我們將檢查一個執行以下步驟的簡單任務:

  1. 驗證用戶的用戶名和密碼。
  2. 獲取用戶的應用程序角色。
  3. 記錄用戶的應用程序訪問時間。

方法一:回調地獄(“末日金字塔”)

同步這些調用的古老解決方案是通過嵌套回調。 對於簡單的異步 JavaScript 任務來說,這是一種不錯的方法,但由於稱為回調地獄的問題而無法擴展。

插圖:異步 JavaScript 回調地獄反模式

這三個簡單任務的代碼如下所示:

 const verifyUser = function(username, password, callback){ dataBase.verifyUser(username, password, (error, userInfo) => { if (error) { callback(error) }else{ dataBase.getRoles(username, (error, roles) => { if (error){ callback(error) }else { dataBase.logAccess(username, (error) => { if (error){ callback(error); }else{ callback(null, userInfo, roles); } }) } }) } }) };

每個函數都有一個參數,該參數是另一個函數,該函數使用參數調用,該參數是前一個操作的響應。

太多的人僅僅閱讀上面的句子就會經歷大腦凍結。 擁有一個包含數百個類似代碼塊的應用程序會給維護代碼的人帶來更多麻煩,即使他們是自己編寫的。

一旦您意識到database.getRoles是另一個具有嵌套回調的函數,這個示例就會變得更加複雜。

 const getRoles = function (username, callback){ database.connect((connection) => { connection.query('get roles sql', (result) => { callback(null, result); }) }); };

除了代碼難以維護之外,DRY 原則在這種情況下絕對沒有任何價值。 例如,在每個函數中重複錯誤處理,並且從每個嵌套函數調用主回調。

更複雜的異步 JavaScript 操作,例如循環異步調用,是一個更大的挑戰。 事實上,使用回調沒有簡單的方法可以做到這一點。 這就是為什麼像 Bluebird 和 Q 這樣的 JavaScript Promise 庫如此受歡迎的原因。 它們提供了一種對語言本身尚未提供的異步請求執行常見操作的方法。

這就是原生 JavaScript Promises 的用武之地。

JavaScript 承諾

Promise 是逃離回調地獄的下一個合乎邏輯的步驟。 這種方法並沒有取消回調的使用,但它使函數鏈接變得簡單並簡化了代碼,使其更易於閱讀。

插圖:異步 JavaScript Promise 圖

有了 Promises,我們的異步 JavaScript 示例中的代碼將如下所示:

 const verifyUser = function(username, password) { database.verifyUser(username, password) .then(userInfo => dataBase.getRoles(userInfo)) .then(rolesInfo => dataBase.logAccess(rolesInfo)) .then(finalResult => { //do whatever the 'callback' would do }) .catch((err) => { //do whatever the error handler needs }); };

為了實現這種簡單性,示例中使用的所有函數都必須是Promisified 。 讓我們看一下如何更新getRoles方法以返回Promise

 const getRoles = function (username){ return new Promise((resolve, reject) => { database.connect((connection) => { connection.query('get roles sql', (result) => { resolve(result); }) }); }); };

我們修改了該方法以返回一個帶有兩個回調的Promise ,並且Promise本身執行該方法中的操作。 現在, resolvereject回調將分別映射到Promise.thenPromise.catch方法。

您可能會注意到getRoles方法在內部仍然容易出現末日金字塔現象。 這是由於創建數據庫方法的方式,因為它們不返回Promise 。 如果我們的數據庫訪問方法也返回Promise ,那麼getRoles方法將如下所示:

 const getRoles = new function (userInfo) { return new Promise((resolve, reject) => { database.connect() .then((connection) => connection.query('get roles sql')) .then((result) => resolve(result)) .catch(reject) }); };

方法 3:異步/等待

隨著 Promises 的引入,厄運金字塔得到了顯著緩解。 但是,我們仍然必須依賴傳遞給Promise.then.catch方法的回調。

Promise 為 JavaScript 中最酷的改進之一鋪平了道路。 ECMAScript 2017 以asyncawait語句的形式在 JavaScript 的 Promise 之上引入了語法糖。

它們允許我們編寫基於Promise的代碼,就好像它是同步的一樣,但不會阻塞主線程,如下代碼示例所示:

 const verifyUser = async function(username, password){ try { const userInfo = await dataBase.verifyUser(username, password); const rolesInfo = await dataBase.getRoles(userInfo); const logStatus = await dataBase.logAccess(userInfo); return userInfo; }catch (e){ //handle errors as needed } };

Awaiting Promise to resolve 只允許在async函數中使用,這意味著verifyUser必須使用async function來定義。

但是,一旦進行了這個小更改,您就可以await任何Promise ,而無需對其他方法進行額外更改。

異步 - 一個期待已久的承諾解決方案

異步函數是 JavaScript 異步編程發展的下一個合乎邏輯的步驟。 它們將使您的代碼更清晰,更易於維護。 將函數聲明為async將確保它始終返回Promise ,因此您不必再擔心這一點。

為什麼要從今天開始使用 JavaScript async函數?

  1. 生成的代碼更乾淨。
  2. 錯誤處理要簡單得多,它依賴於try / catch ,就像在任何其他同步代碼中一樣。
  3. 調試要簡單得多。 在.then塊內設置斷點不會移動到下一個.then因為它只會逐步執行同步代碼。 但是,您可以單步await調用,就好像它們是同步調用一樣。