天天看點

Tasks, microtasks, queues and schedules

有些東西不時常提起很可能會遺忘掉,建議自己經常記錄一些!非原文 表示 個人見解,文章翻譯部分章節。 本文翻譯自 https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/  感謝jake提供的分享,版權歸原作者所有,僅供學習參考使用!

Try it

  首先來看一段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
           

正确答案是 

script start

script end

promise1

promise2

setTimeout 

原文中提到可能浏覽器的差異 可能setTimeout 會在 promise1 與 promise2之前列印,在firefox與Safari 8.0.7中總是正确的。(非原文:浏覽器的差異本文中不考慮,具體檢視原文)。

Why this happens

要了解這一點,您需要知道事件循環(event loop)如何處理任務和微任務。當你第一次遇到它的時候,你可能會有很多的想法。

請保持深呼吸……

每個“線程”都有自己的事件循環(event loop),是以每個web worker都有自己的事件循環(event loop),是以它可以獨立執行,然而,同源上的所有windows視窗都共享一個事件循環(event loop),保證它們可以同步通信。事件循環(event loop)持續 執行隊列中的任何任務。事件循環(event loop)可以有多個任務源,也需確定各個任務源的執行順序(IndexedDB等規範定義了它們自己的執行順序),但是浏覽器可以決定在循環的每個回合中選擇哪個源來執行任務(非本文:回答為什麼會有不同的執行順序)。這允許浏覽器優先處理性能敏感的任務,比如使用者輸入等。

Task 任務被排程,這樣浏覽器就可以從内部擷取JavaScript/DOM,并確定這些操作按順序發生。在任務之間,浏覽器可能呈現更新。從滑鼠單擊到事件回調需要排程任務,解析HTML以及上述setTimeout 一樣。setTimeout等待一個給定的延遲,然後為它的回調安排一個新的任務。這就是為什麼在script end後列印 setTimeout,因為列印script end 是第一個任務的一部分,并且setTimeout記錄在一個單獨的任務中。

 MicroTask 微任務通常被安排在目前執行腳本之後應該立即發生的事情上,例如對一批操作做出反應,或者使某些東西異步執行,而不需要承擔整個新任務的代價。隻要沒有其他JavaScript在執行過程中,并且在每個任務的末尾,微任務隊列在回調之後被處理。在微任務期間排隊等待的任何其他微任務都被添加到隊列的末尾并被處理。微任務包括 mutation observer 回調, promise 回調等。 一旦一個promise設定了,或者它已經resolve,就把它加入microTask隊列中。這確定了promise 回調是異步的,即使promise 已經resolve。調用 .then(yey, nay)  讓promise任務立即步入 microtack的任務隊列中。這就是為什麼promise1和promise2會在 script end 之後被列印,因為目前運作的腳本必須在微任務處理之前完成。promise1和promise2在setTimeout之前被列印,因為微任務總是在下一個任務之前發生。

STEPS

一步一步的圖示

console.log('script start')
           
 Tasks Run script 執行JS 代碼,列印輸出script start
MicroTasks
JS stack script
Log script start
setTimeout(function(){
  console.log('setTimeout')
},0)

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});
           
 Tasks Run script  setTimeout-callback

setTimeout的回調會被加入到Task中,

promise 回調加入到Microtasks中

MicroTasks promise then
JS stack script
Log script start
console.log('script end')
           
 Tasks Run script  setTimeout-callback

列印 script end, 已經到最後一行代碼了

JS stack 中執行完成,開始執行micro task

Run script一緻存在一直在目前任務執行完成後

MicroTasks promise then
JS stack
Log script start  script end

執行microTask任務

 Tasks Run script  setTimeout callback

執行完成第一個promise 後return undefined,

繼續 将下一個promise callback 加入micro task

MicroTasks promise then promise then
JS stack promise callback
Log script start  script end promise1
Tasks Run script  setTimeout callback 執行完之後清空
MicroTasks  promise then
JS stack
log script start  script end promise1

重複以上步驟,執行完promise then 列印 promise2 後

Tasks Run script  setTimeout callback
MicroTasks
JS stack
Log script start  script end promise1 promise2

    上面提到過:微任務總是在下一個任務之前發生 。所有的微任務執行完成後,在執行下一個任務之前(例如,浏覽器預備重新渲染之前)此時第一個RunScript的任務也即是 執行完成了。

Tasks  setTimeout callback
MicroTasks
JS stack
log script start  script end promise1 promise2
Tasks  setTimeout callback setTimeout callback 任務進入JS stack
MicroTasks
JS stack setTimeout callback
Log script start  script end promise1 promise2 setTimeout
Tasks 結束
MicroTasks
JS stack
Log script start  script end promise1 promise2 setTimeout

----------------------------------------------------------------------------------------------------------------------------------------

接下來繼續分析另外一段代碼

<div class="outer">
  <div class="inner"></div>
</div>
           
// 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);
           

mutationObser主要用來監聽DOM的變動,callback會在每次DOM變動後調用,observe中第一個參數表示監聽的DOM元元的article 第二個表示變動的類型 attribute:true表示監聽屬性的變動;前文提到過mutationObserve也是microTask。

繼續分析下:觸發click事件,setTimeout  是tasks,  mutation observe和 promise是microtasks

Tasks Dispatch click  setTimeout callback

觸發事件後,接收JS stack目前為onClick ,分析onClick函數中的代碼執行将任務配置設定如下,console.log直接執行,輸出click

pormise 加入microtasks

修改DOM後mutationObserver的任務也會加入到microtask中

MicroTasks promise then Mutationobservers
JS stack onClick
Log click

onClick已經執行完,微任務總是在下一個任務之前發生

Tasks Dispatch click  setTimeout callback 在JS stack為空的時候執行下一個任務前執行 micro task,具體分析如上
MicroTasks
JS stack
Log click promise mutate

但是由于冒泡的原因 會再次的onCick回調

Tasks

Dispatch click  setTimeout callback

setTimeout callback

注意新加的setTimeout callback
MicroTasks promise then  mutation observers
JS stack onClick
Log click promise mutate click
Tasks

Dispatch click  setTimeout callback

setTimeout callback

執行邏輯同上
MicroTasks
JS stack
Log click promise mutate click promise mutate
Tasks 沒有微任務後會繼續執行下一個任務
MicroTasks
JS stack
Log click promise mutate click promise mutate timeout timeout

---------------------------------------------------------------------------------------------------------

加深了印象之後進一步的深入

inner.click
           

注直接click的時候與Run Script是同步的,之前的是runScript 之後 dispatch click;在JS stack為空的時候執行下一個任務前執行 micro task

Tasks Run script setTimeout callback

在執行完innerClick後, 此時stack并沒有空,冒泡 會觸發另一個 onClick,此時仍然實在Run Script階段

mutation observer已經pending無法加入再次加入microtask

MicroTasks promise then Mutation observe promise then
JS stack script  onClick
Log click

一切隻有當同步的執行完之後,Run script結束 JS stack為空去執行下一個任務時會清空microtask 中的任務,

You made it!

總結:

1.Tasks 按順序執行,浏覽器可以在兩個任務之間進行渲染

2.microTasks 按下規則執行:

   在所有回調執行完成(如:冒泡時會觸發兩次回調) ,并且在沒有其他的JavaScript在運作中的時候

   在每一個任務的最後