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 沒有直接關係,因此它們在教學課程的另一部分中,事件迴圈:微任務和巨集任務 一文中有所介紹。
留言
<code>
標籤,對於多行程式碼 – 將它們包覆在<pre>
標籤中,對於 10 行以上的程式碼 – 使用沙盒 (plnkr、jsbin、codepen…)