摘要: 理解JS的執行順序。
- 作者:前端小智
- 原文:詳解JavaScript的任務、微任務、隊列以及代碼執行順序
思考下面 JavaScript 代碼:
console.log("script start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
Promise.resolve()
.then(function() {
console.log("promise1");
})
.then(function() {
console.log("promise2");
});
console.log("script end");
控制台打印的順序是怎樣的?
答案
正確的答案是:script start
, script end
, promise1
, promise2
, setTimeout
,但是由於瀏覽器實現支持不同導致結果也不一致。
Microsoft Edge、Firefox 40、iOS Safari和桌面Safari 8.0.8 打印promise1
和promise2
之前會先打印 setTimeout
—— 這似乎是瀏覽器廠商相互競爭導致的實現不同。這真的很奇怪,因為 Firefox 39 和 Safari 8.0.7 結果總是正確的。
為什么會這樣
要理解這一點,需要了解事件循環
每個“線程”都有自己的事件循環
事件循環持續運行,直到清空 Tasks 隊列的任務。一個事件循環有多個任務源,這些任務源保證了該源中的執行順序(比如IndexedDB定義了它們自己的規范),但是瀏覽器可以在每次循環中選擇哪個源來執行任務。這允許瀏覽器優先選擇性能敏感的任務,比如用戶輸入等。
Tasks 被放到任務源中,這樣瀏覽器就可以從內部進入JavaScript/DOM領域,並確保這些操作按順序進行。在Tasks 執行期間,瀏覽器可能更新渲染。從鼠標點擊到事件回調需要調度一個任務,解析超文本標記語言也是如此。
setTimeout
遲給定的時間,然后為它的回調調度一個新任務。這就是為什么setTimeout
在打印script end
之后打印,因為打印script end
是第一個任務的一部分,而setTimeout
在一個單獨的任務中。
微任務
只要沒有其他JavaScript處於執行中期,並且在每個任務的末尾,微任務隊列就在回調之后處理。在微任務期間排隊的任何其他微任務都會被添加到隊列的末尾並進行處理。微任務 包括 MutationObserver callbacks。例如上面的例子中的 promise
的 callback
。
一個settled
狀態的promise 或者已經變成settled
狀態(異步請求被settled)的promise,會立刻將它的callback(then)
放到微任務隊列里面。
這確保了 promise
回調是異步的,即便promise
已經變為settled
狀態。因此一個已settled
的promise
調用.then(yey,nay)
時將立即把一個微任務加入微任務隊列中。
這就是為什么promise1
和promise2
會在script end
后打印,因為當前運行的腳本必須在處理微任務之前完成。promise1
和promise2
在setTimeout
之前打印,因為微任務總是在下一個任務之前發生。
好,一步一步的運行:
瀏覽器之間會有什么不同?
一些瀏覽器的打印的順序是 script start, script end, setTimeout, promise1, promise2
。它們在setTimeout
之后運行promise
回調。很可能他們調用promise
回調是作為新任務的一部分,而不是作為一個微任務。
這也是可以理解的,因為promise
來自 ECMAScript 而不是 HTML。ECMAScript 有“作業”的概念,類似於微任務,但是除了模糊的郵件列表討論之外,這種關系並不明確。然而,普遍的共識是,promise
應該是微任務隊列的一部分並且有充足的理由。
將promise
看作任務會導致性能問題,因為回調沒有必要因為任務相關的事(比如渲染)而延遲執行。它還會由於與其他任務源的交互而導致非確定性,並可能中斷與其他api的交互,稍后將詳細介紹。
這里有一條 Edge 反饋,它錯誤地將 promises
當作 任務。WebKit nightly 做對了,所以我認為 Safari 最終會修復,而 Firefox 43 似乎已經修復。
如何判斷某些東西是否使用任務或微任務
動手試一試是一種辦法,查看相對於promise
和setTimeout
如何打印,盡管這取決於實現是否正確。
一種方法是查看規范: 將一個任務加入隊列: step 14 of setTimeout
將 microtask 加入隊列:step 5 of queuing a mutation record
如上所述,ECMAScript 將微任務稱為作業: 調用 EnqueueJob 將一個 微任務加入隊列:step 8.a of PerformPromiseThen
等級一 boss打怪
下面是一段html代碼:
<div class="outer">
<div class="inner"></div>
</div>
給出下面的JS代碼,如果點擊div.inner
將會打印出什么呢?
// Let's get hold of those elements
var outer = document.querySelector(".outer");
var inner = document.querySelector(".inner");
// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
console.log("mutate");
}).observe(outer, {
attributes: true
});
// Here's a click listener…
function onClick() {
console.log("click");
setTimeout(function() {
console.log("timeout");
}, 0);
Promise.resolve().then(function() {
console.log("promise");
});
outer.setAttribute("data-random", Math.random());
}
// …which we'll attach to both elements
inner.addEventListener("click", onClick);
outer.addEventListener("click", onClick);
在偷看答案前先試一試
試一試
和你猜想的有不同嗎?如果是,你得到的結果可能也是正確的。不幸的是,瀏覽器實現並不統一,下面是各個瀏覽器下測試結果:
誰是正確的?
調度'click
'事件是一項任務。 Mutation observer 和 promise 回調被列為微任務。 setTimeout
回調列為任務。 因此運行過程如下:
所以 Chrome 是對的。對我來說新發現是,微任務在回調之后運行(只要沒有其它的 Javascript 在運行),我原以為它只能在一個任務的末尾執行。
瀏覽器出了什么問題?
對於 mutation callbacks,Firefox 和 Safari 都正確地在內部區域和外部區域單擊事件之間執行完畢,清空了微任務隊列,但是 promises
列隊的處理看起來和chrome
不一樣。這多少情有可原,因為作業和微任務的關系不清楚,但是我仍然期望在事件回調之間處理 Firefox ticket. Safari ticket.
對於 Edge,我們已經看到它錯誤的將 promises
當作任務,它也沒有在單擊回調之間清空微任務隊列,而是在所有單擊回調執行完之后清空,於是總共只有一個 mutate
在兩個 click
之后打印。
等級一 boss打怪升級
仍然使用上面的例子,假如我們運行下面代碼會怎么樣:
inner.click();
跟之前一樣,它會觸發 click
事件,但這次是通過 JS 調用的。
試一試
下面是各個瀏覽器的運行情況:
我發誓我一直在從Chrome中得到不同的結果,我已經更新了這張圖表很多次了,我以為我在錯誤地測試Canary。如果你在Chrome中得到不同的結果,請在評論中告訴我是哪個版本。
為什么不同?
應該是這樣的:
所以正確的順序是:click, click, promise, mutate, promise, timeout, timeout,似乎 Chrome 是對的。
以前,這意味着微任務在偵聽器回調之間運行,但.click()
會導致事件同步調度,因此調用.click()
的腳本仍然在回調之間的堆棧中。 上述規則確保微任務不會中斷執行中期的JavaScript。 這意味着我們不處理偵聽器回調之間的微任務隊列,它們在兩個偵聽器之后處理。
總結
任務按順序執行,瀏覽器可以在它們之間進行渲染:
微任務按順序執行,並執行:
- 在每個回調之后,只要沒有其它代碼正在運行。
- 在每個任務的末尾。
關於Fundebug
Fundebug專注於JavaScript、微信小程序、微信小游戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有陽光保險、核桃編程、荔枝FM、掌門1對1、微脈、青團社等眾多品牌企業。歡迎大家免費試用!