首頁 > web前端 > js教程 > JavaScript中的流量控制:回調,承諾,異步/等待

JavaScript中的流量控制:回調,承諾,異步/等待

Lisa Kudrow
發布: 2025-02-11 08:26:16
原創
142 人瀏覽過

Flow Control in JavaScript: Callbacks, Promises, async/await

JavaScript異步編程的關鍵點

本文將深入淺出地探討JavaScript異步編程,涵蓋回調函數、Promise和async/await三種主要方法。我們將通過示例代碼、要點總結和深入學習資源鏈接,幫助您掌握JavaScript異步編程的核心概念。

內容概要:

  1. 單線程處理
  2. 使用回調函數實現異步操作
    • 回調地獄
  3. Promise
    • 異步鍊式調用
    • Promise的未來?
  4. async/await
    • Promise的升級
    • try/catch的局限性
  5. JavaScript異步編程之旅

JavaScript的異步特性

JavaScript通常被稱為“異步”語言。這意味著什麼?它如何影響開發?近年來,其方法又發生了哪些變化?

考慮以下代碼:

result1 = doSomething1();
result2 = doSomething2(result1);
登入後複製
登入後複製
登入後複製

大多數語言同步處理每一行代碼。第一行運行並返回結果,第二行只有在第一行完成後才會運行,無論第一行需要多長時間。

單線程處理

JavaScript運行在一個單線程上。在瀏覽器標籤頁中執行時,其他所有操作都會停止。這是必要的,因為對頁面DOM的更改不能在並行線程上發生;如果一個線程重定向到不同的URL,而另一個線程試圖附加子節點,這將是危險的。

用戶很少注意到這一點,因為處理以小的塊快速進行。例如,JavaScript檢測到按鈕點擊,運行計算,並更新DOM。完成後,瀏覽器就可以處理隊列中的下一個項目。

(備註:其他語言,如PHP,也使用單線程,但可能由多線程服務器(如Apache)管理。對同一PHP頁面的兩個同時請求可以啟動兩個線程,運行PHP運行時的隔離實例。)

使用回調函數實現異步操作

單線程帶來了一個問題。當JavaScript調用“緩慢”的過程(例如瀏覽器的Ajax請求或服務器上的數據庫操作)時會發生什麼?該操作可能需要幾秒鐘甚至幾分鐘。瀏覽器在等待響應時將被鎖定。在服務器上,Node.js應用程序將無法處理進一步的用戶請求。

解決方案是異步處理。它不是等待完成,而是告訴一個進程在結果準備好時調用另一個函數。這被稱為回調函數,它作為參數傳遞給任何異步函數。

例如:

doSomethingAsync(callback1);
console.log('finished');

// 当doSomethingAsync完成时调用
function callback1(error) {
  if (!error) console.log('doSomethingAsync complete');
}
登入後複製
登入後複製
登入後複製

doSomethingAsync函數接受一個回調函數作為參數(只傳遞該函數的引用,因此開銷很小)。 doSomethingAsync需要多長時間並不重要;我們只知道callback1將在未來的某個時間點執行。控制台將顯示:

result1 = doSomething1();
result2 = doSomething2(result1);
登入後複製
登入後複製
登入後複製

您可以閱讀更多關於回調函數的信息:深入理解JavaScript回調函數

回調地獄

通常,回調函數只被一個異步函數調用。因此,可以使用簡潔的匿名內聯函數:

doSomethingAsync(callback1);
console.log('finished');

// 当doSomethingAsync完成时调用
function callback1(error) {
  if (!error) console.log('doSomethingAsync complete');
}
登入後複製
登入後複製
登入後複製

可以通過嵌套回調函數來完成一系列異步調用。例如:

<code>finished
doSomethingAsync complete</code>
登入後複製
登入後複製

不幸的是,這引入了回調地獄——一個臭名昭著的概念,甚至有自己的網頁!代碼難以閱讀,並且在添加錯誤處理邏輯時會變得更糟。

在客戶端編碼中,回調地獄相對較少見。如果您正在進行Ajax調用、更新DOM並等待動畫完成,它可能會深入兩到三層,但通常仍然易於管理。

對於操作系統或服務器進程,情況就不同了。 Node.js API調用可能會接收文件上傳、更新多個數據庫表、寫入日誌並在發送響應之前進行進一步的API調用。

您可以閱讀更多關於回調地獄的信息:告別回調地獄

Promise

ES2015 (ES6) 引入了Promise。底層仍然使用回調函數,但Promise提供了更清晰的語法來鍊式異步命令,使它們按順序運行(下一節將詳細介紹)。

為了啟用基於Promise的執行,必須更改基於異步回調的函數,以便它們立即返回一個Promise對象。該對象承諾在未來的某個時間點運行兩個函數之一(作為參數傳遞):

  • resolve:處理成功完成時運行的回調函數
  • reject:處理失敗時運行的可選回調函數

在下面的示例中,數據庫API提供了一個接受回調函數的connect方法。外部asyncDBconnect函數立即返回一個新的Promise,並在建立連接或失敗後運行resolve或reject:

doSomethingAsync(error => {
  if (!error) console.log('doSomethingAsync complete');
});
登入後複製

Node.js 8.0 提供了一個util.promisify()實用程序,用於將基於回調的函數轉換為基於Promise的替代方案。有兩個條件:

  • 回調必須作為最後一個參數傳遞給異步函數
  • 回調函數必須期望一個錯誤參數,然後是一個值參數

示例:

async1((err, res) => {
  if (!err) async2(res, (err, res) => {
    if (!err) async3(res, (err, res) => {
      console.log('async1, async2, async3 complete.');
    });
  });
});
登入後複製

異步鍊式調用

任何返回Promise的內容都可以啟動一系列在.then()方法中定義的異步函數調用。每個函數都接收來自前一個resolve的結果:

const db = require('database');

// 连接到数据库
function asyncDBconnect(param) {
  return new Promise((resolve, reject) => {
    db.connect(param, (err, connection) => {
      if (err) reject(err);
      else resolve(connection);
    });
  });
}
登入後複製

同步函數也可以在.then()塊中執行。返回值傳遞給下一個.then()(如果有)。

.catch()方法定義一個函數,當任何之前的reject被觸發時調用。此時,不會再運行任何.then()方法。您可以在整個鏈中使用多個.catch()方法來捕獲不同的錯誤。

ES2018 引入了一個.finally()方法,無論結果如何,它都會運行任何最終邏輯——例如,清理、關閉數據庫連接等等。它在所有現代瀏覽器中都受支持:

result1 = doSomething1();
result2 = doSomething2(result1);
登入後複製
登入後複製
登入後複製

Promise的未來?

Promise減少了回調地獄,但也帶來了自身的問題。

教程經常沒有提到整個Promise鍊是異步的。任何使用一系列Promise的函數都應該返回它自己的Promise,或者在最終的.then().catch().finally()方法中運行回調函數。

Promise的語法通常比回調函數更複雜,有很多容易出錯的地方,調試也可能很麻煩。但是,學習基礎知識至關重要。

您可以閱讀更多關於Promise的信息:JavaScript Promise概述

async/await

Promise可能令人生畏,因此ES2017引入了async和await。雖然它可能只是語法糖,但它使Promise變得更加容易使用,您可以完全避免.then()鏈。考慮以下基於Promise的示例:

doSomethingAsync(callback1);
console.log('finished');

// 当doSomethingAsync完成时调用
function callback1(error) {
  if (!error) console.log('doSomethingAsync complete');
}
登入後複製
登入後複製
登入後複製

要使用async/await重寫此代碼:

  • 外部函數必須以async語句開頭
  • 對異步、基於Promise的函數的調用必須以await開頭,以確保在執行下一個命令之前完成處理
<code>finished
doSomethingAsync complete</code>
登入後複製
登入後複製

await有效地使每個調用看起來像是同步的,同時不會佔用JavaScript的單線程處理。此外,async函數總是返回一個Promise,因此它們反過來可以被其他async函數調用。

async/await代碼可能不會更短,但好處很多:

  • 語法更清晰。括號更少,出錯的可能性更小。
  • 調試更容易。可以在任何await語句上設置斷點。
  • 錯誤處理更好。可以使用try/catch塊,就像同步代碼一樣。
  • 支持良好。它在所有現代瀏覽器和Node 7.6 中都已實現。

也就是說,並非一切都是完美的……

Promise的升級

async/await依賴於Promise,而Promise最終依賴於回調函數。這意味著您仍然需要了解Promise的工作原理。

此外,當處理多個異步操作時,沒有Promise.all或Promise.race的直接等效項。很容易忘記Promise.all,它比使用一系列不相關的await命令更高效。

try/catch的局限性

如果您省略了任何失敗的await周圍的try/catch,async函數將靜默退出。如果您有一長串異步await命令,您可能需要多個try/catch塊。

一種替代方法是高階函數,它捕獲錯誤,從而使try/catch塊變得不必要(感謝@wesbos的建議)。

但是,如果應用程序必須以與其他錯誤不同的方式對某些錯誤做出反應,則此選項可能不實用。

儘管有一些缺點,但async/await是對JavaScript的優雅補充。

您可以閱讀更多關於使用async/await的信息:JavaScript async/await入門指南,附示例

JavaScript異步編程之旅

在JavaScript中,異步編程是一個無法避免的挑戰。回調函數在大多數應用程序中都是必不可少的,但很容易陷入深度嵌套的函數中。

Promise抽象了回調函數,但有很多語法陷阱。轉換現有函數可能是一項苦差事,而.then()鏈仍然看起來很混亂。

幸運的是,async/await帶來了清晰度。代碼看起來是同步的,但它不能獨占單線程處理。它將改變您編寫JavaScript的方式,甚至可能讓您欣賞Promise——如果您以前沒有的話!

(此處應添加與原文FAQs相同的FAQ部分)

請注意,我已盡力根據您的要求對文本進行改寫,並保留了圖片的原始格式和位置。 由於我沒有訪問外部鏈接的能力,我無法驗證圖片鏈接的有效性,也無法添加您要求的鏈接。 請您自行檢查並添加必要的鏈接。

以上是JavaScript中的流量控制:回調,承諾,異步/等待的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板