原文:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
英語水平有限,翻譯中可能有用詞不當,望指出
前言
事實上,如果你更喜歡視頻學習,Philip Roberts有一個關于event loop 的很棒的演講--盡管沒有涉及微任務,但其他部分的介紹還是很值得學習的。好了,繼續(xù)我們的話題。
來看一個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');
上述代碼中,日志將以什么樣的順序出現(xiàn)呢?
1. 試一試!
可以將以上代碼復制到瀏覽器的控制臺看一下輸出結果.
正確的順序是:script start,script end,promise1,promise2,setTimeout。
文章中說不同瀏覽器的表現(xiàn)不一樣,文章寫于2015年,本機測試火狐和safari瀏覽器里的測試結果均與chrome一致。因此不再翻譯這一部分。
2. 為什么會這樣呢?
為了理解這個,你需要知道事件循環(huán)是怎么處理宏任務和微任務的。第一次遇到你可能會覺得很頭大。深呼吸,我們繼續(xù)...
每一個‘線程’都有一個自己的事件循環(huán)來保證其獨立運行,web worker也是一樣的,而所有的同源窗口擁有同一個 event loop 以便他們可以同步通訊。事件循環(huán)不斷地處理任務隊列中的任務。一個 event loop 可以有多個任務源,而這些任務源保證了來源于它的任務的執(zhí)行順序(就像 IndexedDB那樣定義了自己的規(guī)范),但是瀏覽器在每輪循環(huán)的時候可以選擇執(zhí)行哪個任務源。這使瀏覽器可以優(yōu)先處理對性能敏感的任務,比如用戶輸入。
2.1 宏任務(Tasks)
任務隊列使得瀏覽器可以從內部(?)訪問 Javascript/DOM 并確保這些操作有序發(fā)生(其實這句話我不是很清楚該怎么翻譯原文是:Tasks are scheduled so the browser can get from its internals into JavaScript/DOM land and ensures these actions happen sequentially.)。在兩個任務的間隙,瀏覽器可能會進行更新渲染。鼠標點擊觸發(fā)事件回調需要執(zhí)行一個宏任務,就像解析HTML一樣,在上述例子的代碼中setTimeout也是。
setTimeout 會在一段指定時間后執(zhí)行,然后為它的回調函數(shù)創(chuàng)建一個新的宏任務。這就是為什么setTimeout會在script end之后輸出,因為輸出script end是第一個任務的內容,而setTimeout是在另一個任務中輸出的。
2.2 微任務(microTask)
微任務通常是當前腳本執(zhí)行完后要立即執(zhí)行的內容,比如對一批操作做出響應,或者做一些異步處理。在每一個宏任務的最后,只要執(zhí)行棧中沒有需要執(zhí)行的 Javascript ,就會在回調結束后處理微任務隊列。在這一階段(我:一個宏任務結束處理微任務階段)產生的微任務都會被加入到微任務隊列末尾,并且會在這一階段處理(不需要等到下次宏任務結束)。微任務有包括MutationObserver的回調,以及開篇例子中的promise的回調。
一旦promise有了處理結果(resolve,reject),或者已經有了處理結果,就會為它對應的回調函數(shù)創(chuàng)建一個微任務(resolve->.then, reject->.catch)。這樣可以確保promise是異步的,盡管他已經拿到了處理結果。因此在promise有結果后,調用.then(yey,nay)會立即創(chuàng)建一個微任務。這就是為什么promise1和promise2會在script end之后輸出,因為在微任務處理之前必須要先處理完當前的腳本。promise1和promise2會在setTimeout之前輸出是因為微任務會在下一個任務之前處理。
3. 如何辨別宏任務和微任務呢?
測試是一種方案。參考promise&setTimeout觀察日志的輸出,但是你要保證瀏覽器的實現(xiàn)是正確的。
穩(wěn)妥的方案是查閱spec。
4. 測試題
下面是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);
在看答案之前先試一試,提示:日志可以被打印多次
5. 答案
本機瀏覽器Chrome,Safari,火狐輸出如下
click
promise
mutate
click
promise
mutate
timeout
timeout
分發(fā)click事件是一個宏任務,MutationObserver和promise的回調是微任務隊列中的,setTimeout是一個宏任務。
我才知道微任務是在回調之后進行處理的(只要執(zhí)行棧中沒有其他的Javascript),之前我認為他就是宏任務結束后處理的。這個規(guī)則來源于HTML spec關于調用一個回調函數(shù)的說明:

微任務檢查點會檢查整個微任務隊列,除非我們已經在處理微任務隊列,同樣的,ECMAScript里這樣說:

瀏覽器差異的問題不翻譯
6. 測試題升級
依然使用第4部分中的例子,如過我們執(zhí)行下面的內容會發(fā)生什么?
inner.click();
這次會像之前一樣進行事件分發(fā)處理,但這次用的是腳本而不是真正的交互。
7. 升級題目答案
click
click
promise
mutate
promise
timeout
timeout
8. 為什么兩個結果會有差異?
當每個監(jiān)聽回調被調用后

先前的結果中,微任務在兩次監(jiān)聽回調之間,但是
.click()導致事件分發(fā)是同步進行的,因此.click()的調用在兩個監(jiān)聽回調之間依然在執(zhí)行棧中。上述規(guī)則保證微任務不會中斷Javascript的執(zhí)行。這意味著,我們不是在兩個監(jiān)聽回調之間處理微任務而是處理完兩個監(jiān)聽回調之后處理微任務。
IndexedDB相關不懂,不翻譯
9. 總結
- 宏任務按順序執(zhí)行,瀏覽器可能會在兩個任務間隙進渲染更新
- 微任務按順序執(zhí)行,并且:
- 在每一個回調之后,只要執(zhí)行棧中沒有其他的js
- 在每一個任務結尾