【翻譯】Tasks, microtasks, queues and schedules

原文: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)建一個微任務。這就是為什么promise1promise2會在script end之后輸出,因為在微任務處理之前必須要先處理完當前的腳本。promise1promise2會在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事件是一個宏任務,MutationObserverpromise的回調是微任務隊列中的,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í)行,并且:
    1. 在每一個回調之后,只要執(zhí)行棧中沒有其他的js
    2. 在每一個任務結尾
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容