摘要: 理解JS的執(zhí)行順序零渐。
- 作者:前端小智
- 原文:詳解JavaScript的任務窒舟、微任務、隊列以及代碼執(zhí)行順序
思考下面 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
惠豺,但是由于瀏覽器實現(xiàn)支持不同導致結(jié)果也不一致银还。
Microsoft Edge、Firefox 40洁墙、iOS Safari和桌面Safari 8.0.8 打印promise1
和promise2
之前會先打印 setTimeout
—— 這似乎是瀏覽器廠商相互競爭導致的實現(xiàn)不同见剩。這真的很奇怪,因為 Firefox 39 和 Safari 8.0.7 結(jié)果總是正確的扫俺。
為什么會這樣
要理解這一點苍苞,需要了解事件循環(huán)<event loop>如何處理任務和微任務。
每個“線程”都有自己的事件循環(huán)<event loop>狼纬,因此每個 web worker 都有自己的事件循環(huán)羹呵,因此可以獨立執(zhí)行,而來自同域的所有窗口共享一個事件循環(huán)疗琉,所以它們可以同步地通信冈欢。
事件循環(huán)持續(xù)運行,直到清空 Tasks 隊列的任務盈简。一個事件循環(huán)有多個任務源凑耻,這些任務源保證了該源中的執(zhí)行順序(比如IndexedDB定義了它們自己的規(guī)范),但是瀏覽器可以在每次循環(huán)中選擇哪個源來執(zhí)行任務柠贤。這允許瀏覽器優(yōu)先選擇性能敏感的任務香浩,比如用戶輸入等。
Tasks 被放到任務源中臼勉,這樣瀏覽器就可以從內(nèi)部進入JavaScript/DOM領(lǐng)域邻吭,并確保這些操作按順序進行。在Tasks 執(zhí)行期間宴霸,瀏覽器可能更新渲染囱晴。從鼠標點擊到事件回調(diào)需要調(diào)度一個任務,解析超文本標記語言也是如此瓢谢。
setTimeout
遲給定的時間畸写,然后為它的回調(diào)調(diào)度一個新任務。這就是為什么setTimeout
在打印script end
之后打印氓扛,因為打印script end
是第一個任務的一部分枯芬,而setTimeout
在一個單獨的任務中。
微任務<Microtasks>通常是針對當前執(zhí)行腳本之后應該立即發(fā)生的事情進行調(diào)度的幢尚,比如對一批操作進行響應破停,或者在不影響整個新任務的情況下進行異步處理翅楼。
只要沒有其他JavaScript處于執(zhí)行中期尉剩,并且在每個任務的末尾,微任務隊列就在回調(diào)之后處理毅臊。在微任務期間排隊的任何其他微任務都會被添加到隊列的末尾并進行處理理茎。微任務 包括 MutationObserver callbacks。例如上面的例子中的 promise
的 callback
。
一個settled
狀態(tài)的promise 或者已經(jīng)變成settled
狀態(tài)(異步請求被settled)的promise皂林,會立刻將它的callback(then)
放到微任務隊列里面朗鸠。
這確保了 promise
回調(diào)是異步的,即便promise
已經(jīng)變?yōu)?code>settled狀態(tài)础倍。因此一個已settled
的promise
調(diào)用.then(yey,nay)
時將立即把一個微任務加入微任務隊列中烛占。
這就是為什么promise1
和promise2
會在script end
后打印,因為當前運行的腳本必須在處理微任務之前完成沟启。promise1
和promise2
在setTimeout
之前打印忆家,因為微任務總是在下一個任務之前發(fā)生。
好德迹,一步一步的運行:
瀏覽器之間會有什么不同芽卿?
一些瀏覽器的打印的順序是 script start, script end, setTimeout, promise1, promise2
。它們在setTimeout
之后運行promise
回調(diào)胳搞。很可能他們調(diào)用promise
回調(diào)是作為新任務的一部分卸例,而不是作為一個微任務。
這也是可以理解的肌毅,因為promise
來自 ECMAScript 而不是 HTML筷转。ECMAScript 有“作業(yè)”的概念,類似于微任務悬而,但是除了模糊的郵件列表討論之外旦装,這種關(guān)系并不明確。然而摊滔,普遍的共識是阴绢,promise
應該是微任務隊列的一部分并且有充足的理由。
將promise
看作任務會導致性能問題艰躺,因為回調(diào)沒有必要因為任務相關(guān)的事(比如渲染)而延遲執(zhí)行呻袭。它還會由于與其他任務源的交互而導致非確定性,并可能中斷與其他api的交互腺兴,稍后將詳細介紹左电。
這里有一條 Edge 反饋,它錯誤地將 promises
當作 任務页响。WebKit nightly 做對了篓足,所以我認為 Safari 最終會修復,而 Firefox 43 似乎已經(jīng)修復闰蚕。
如何判斷某些東西是否使用任務或微任務
動手試一試是一種辦法栈拖,查看相對于promise
和setTimeout
如何打印,盡管這取決于實現(xiàn)是否正確没陡。
一種方法是查看規(guī)范: 將一個任務加入隊列: step 14 of setTimeout
將 microtask 加入隊列:step 5 of queuing a mutation record
如上所述涩哟,ECMAScript 將微任務稱為作業(yè): 調(diào)用 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);
在偷看答案前先試一試
試一試
和你猜想的有不同嗎贴彼?如果是潜腻,你得到的結(jié)果可能也是正確的。不幸的是器仗,瀏覽器實現(xiàn)并不統(tǒng)一融涣,下面是各個瀏覽器下測試結(jié)果:
誰是正確的?
調(diào)度'click
'事件是一項任務。 Mutation observer 和 promise 回調(diào)被列為微任務精钮。 setTimeout
回調(diào)列為任務暴心。 因此運行過程如下:
所以 Chrome 是對的。對我來說新發(fā)現(xiàn)是杂拨,微任務在回調(diào)之后運行(只要沒有其它的 Javascript 在運行)专普,我原以為它只能在一個任務的末尾執(zhí)行。
瀏覽器出了什么問題弹沽?
對于 mutation callbacks檀夹,F(xiàn)irefox 和 Safari 都正確地在內(nèi)部區(qū)域和外部區(qū)域單擊事件之間執(zhí)行完畢,清空了微任務隊列策橘,但是 promises
列隊的處理看起來和chrome
不一樣炸渡。這多少情有可原,因為作業(yè)和微任務的關(guān)系不清楚丽已,但是我仍然期望在事件回調(diào)之間處理 Firefox ticket. Safari ticket.
對于 Edge蚌堵,我們已經(jīng)看到它錯誤的將 promises
當作任務,它也沒有在單擊回調(diào)之間清空微任務隊列沛婴,而是在所有單擊回調(diào)執(zhí)行完之后清空吼畏,于是總共只有一個 mutate
在兩個 click
之后打印。
等級一 boss打怪升級
仍然使用上面的例子嘁灯,假如我們運行下面代碼會怎么樣:
inner.click();
跟之前一樣泻蚊,它會觸發(fā) click
事件,但這次是通過 JS 調(diào)用的丑婿。
試一試
下面是各個瀏覽器的運行情況:
我發(fā)誓我一直在從Chrome中得到不同的結(jié)果性雄,我已經(jīng)更新了這張圖表很多次了,我以為我在錯誤地測試Canary羹奉。如果你在Chrome中得到不同的結(jié)果秒旋,請在評論中告訴我是哪個版本。
為什么不同诀拭?
應該是這樣的:
所以正確的順序是:click, click, promise, mutate, promise, timeout, timeout迁筛,似乎 Chrome 是對的。
以前炫加,這意味著微任務在偵聽器回調(diào)之間運行瑰煎,但.click()
會導致事件同步調(diào)度铺然,因此調(diào)用.click()
的腳本仍然在回調(diào)之間的堆棧中俗孝。 上述規(guī)則確保微任務不會中斷執(zhí)行中期的JavaScript酒甸。 這意味著我們不處理偵聽器回調(diào)之間的微任務隊列,它們在兩個偵聽器之后處理赋铝。
總結(jié)
任務按順序執(zhí)行插勤,瀏覽器可以在它們之間進行渲染:
微任務按順序執(zhí)行,并執(zhí)行:
- 在每個回調(diào)之后革骨,只要沒有其它代碼正在運行玩裙。
- 在每個任務的末尾洗搂。
關(guān)于Fundebug
Fundebug專注于JavaScript、微信小程序、微信小游戲枉圃、支付寶小程序、React Native虐块、Node.js和Java線上應用實時BUG監(jiān)控吨艇。 自從2016年雙十一正式上線,F(xiàn)undebug累計處理了10億+錯誤事件巍实,付費客戶有陽光保險滓技、核桃編程、荔枝FM棚潦、掌門1對1令漂、微脈、青團社等眾多品牌企業(yè)丸边。歡迎大家免費試用叠必!