2021 年 12 月 12 日

微任務

Promise 處理函式 .then/.catch/.finally 永遠都是非同步的。

即使 Promise 立即解析,位於 .then/.catch/.finally 下方 的程式碼行仍然會在這些處理函式之前執行。

以下是一個示範

let promise = Promise.resolve();

promise.then(() => alert("promise done!"));

alert("code finished"); // this alert shows first

如果你執行它,你會先看到 code finished,然後才是 promise done!

這很奇怪,因為 Promise 肯定從一開始就已經完成了。

為什麼 .then 會在之後觸發?發生了什麼事?

微任務佇列

非同步任務需要適當的管理。為此,ECMA 標準指定了一個內部佇列 PromiseJobs,更常稱為「微任務佇列」(V8 術語)。

正如 規格 中所述

  • 佇列是先進先出:先排入佇列的任務會先執行。
  • 只有當沒有其他任務在執行時,才會開始執行任務。

或者,更簡單地說,當一個 Promise 準備好時,它的 .then/catch/finally 處理常式會被放入佇列中;它們尚未執行。當 JavaScript 引擎從目前的程式碼中釋放出來時,它會從佇列中取出一個任務並執行它。

這就是為什麼上面的範例中「程式碼已完成」會先顯示。

Promise 處理常式總是會經過這個內部佇列。

如果有一個包含多個 .then/catch/finally 的鏈,那麼它們每一個都會非同步執行。也就是說,它會先排入佇列,然後在目前的程式碼完成且先前排入佇列的處理常式完成後執行。

如果順序對我們很重要呢?我們如何讓 程式碼已完成 出現在 promise 已完成 之後?

很簡單,只要用 .then 將它放入佇列中

Promise.resolve()
  .then(() => alert("promise done!"))
  .then(() => alert("code finished"));

現在順序就如預期般。

未處理的拒絕

還記得文章 使用 Promise 處理錯誤 中的 unhandledrejection 事件嗎?

現在我們可以確切地看到 JavaScript 如何找出有未處理的拒絕。

當 Promise 錯誤未在微任務佇列的結尾處處理時,就會發生「未處理的拒絕」。

通常,如果我們預期會發生錯誤,我們會在 Promise 鏈中加入 .catch 來處理它

let promise = Promise.reject(new Error("Promise Failed!"));
promise.catch(err => alert('caught'));

// doesn't run: error handled
window.addEventListener('unhandledrejection', event => alert(event.reason));

但是如果我們忘記加入 .catch,那麼在微任務佇列為空後,引擎會觸發事件

let promise = Promise.reject(new Error("Promise Failed!"));

// Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));

如果我們稍後處理錯誤呢?像這樣

let promise = Promise.reject(new Error("Promise Failed!"));
setTimeout(() => promise.catch(err => alert('caught')), 1000);

// Error: Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));

現在,如果我們執行它,我們會先看到 Promise 已失敗!,然後看到 已捕捉

如果我們不知道微任務佇列,我們可能會疑惑:「為什麼 unhandledrejection 處理常式會執行?我們已經捕捉並處理錯誤了!」

但現在我們了解到,當微任務佇列完成時,unhandledrejection 會產生:引擎會檢查 Promise,如果其中任何一個處於「已拒絕」狀態,則會觸發事件。

在上面的範例中,由 setTimeout 加入的 .catch 也會觸發。但它會稍後觸發,在 unhandledrejection 已經發生之後,所以它不會改變任何事情。

摘要

Promise 處理總是異步的,因為所有 promise 動作都會通過內部的「promise 工作」佇列,也稱為「微任務佇列」(V8 術語)。

所以 .then/catch/finally 處理常式總是在當前程式碼完成後才被呼叫。

如果我們需要保證某段程式碼在 .then/catch/finally 之後執行,我們可以將它新增到串接的 .then 呼叫中。

在包括瀏覽器和 Node.js 在內的大多數 JavaScript 引擎中,微任務的概念與「事件迴圈」和「巨集任務」緊密相關。由於這些與 promise 沒有直接關係,因此它們在教學課程的另一部分中,事件迴圈:微任務和巨集任務 一文中有所介紹。

教學課程地圖

留言

留言前請先閱讀…
  • 如果您有改進建議 - 請 提交 GitHub 問題 或提交拉取請求,而不是留言。
  • 如果您無法理解文章中的某些內容 – 請詳細說明。
  • 要插入幾行程式碼,請使用 <code> 標籤,對於多行程式碼 – 將它們包覆在 <pre> 標籤中,對於 10 行以上的程式碼 – 使用沙盒 (plnkrjsbincodepen…)