瀏覽器環境下的microtaks和macrotasks

帶有可視代碼執行順序的原文連接 https://jakearchibald.com/201...
此篇文字並不是其完整翻譯,加入了一部分本身的理解,好比將其中的task替換爲macrotask或是刪除了可視代碼執行順序的逐步解釋。

運行順序

參考如下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 中,setTimeout會優先於promise1promise2。而使人奇怪的是,在 Firefox 39 和 Safari 8.0.7 中又是一致的。html

爲何會這樣

  1. Macrotask

想要理解這部份內容,你須要知道事件循環和microtasks。若是你是第一次接觸相關內容,可能會須要一些精力,別緊張,你們都會這樣,深呼吸…java

在瀏覽器中,每個thread(能夠理解爲每個頁籤)都有本身的事件循環,所以,它們能夠相互獨立執行自身的Macrotask,然而,同源的窗口會分享同一個事件循環來保證相互能夠進行同步通信行爲。事件循環會持續運行下去,用於執行當前存在的全部任務列表。每個事件循環存在多個不一樣的任務隊列用以保證執行順序,而瀏覽器會依照任務類別來從任務序列中選取一個任務來進行執行。這使得瀏覽器能夠優先選擇執行更爲重要的任務,好比用戶輸入操做。web

Macrotask是已經被排序完成的,所以瀏覽器能夠經過內部的機制來直接將其放置於javascript/DOM程序域中並確保每個程序步驟的順序執行。而在兩個任務執行間隔之中,瀏覽器 可能 會執行更新操做。好比處理獲取用戶點擊的回調函數,分析HTML,又或者是setTimeout算法

setTimeout等待一個指定的時間延遲而後加入一個新的任務來執行對應的回調函數。這就是爲何setTimeout會延遲於script end,由於script end是第一個任務的程序內容,而setTimeout是來以後續的另外一個任務。api

  1. Microtasks

Microtasks一般用於排列那些應當在當前任務執行完畢後當即執行的任務,好比對某些事件做出反應,或是一些不會影響新任務的異步操做。這個Microtasks序列是在沒有其餘JavaScript任務正在執行,同時在其餘Macrotask執行完畢以後。任何新添加的Microtasks會被排列到Microtasks的隊尾並進行處理。promise的回調函數正是處於Microtasks隊列之中。promise

當一個promise結束掉之後,或者它在以前已經處理完畢,那麼會添加一個回饋結果的回調函數至Microtasks的隊尾。這確保了promise的回調函數永遠是異步執行的,即便promise已經在當前的時間片執行完畢。所以在調用.then(yey,nay)時並不會直接將一個Macrotask添加至隊尾。這就是爲何promise1promise2會晚於script end,當前運行的Macrotask必定會在Macrotask處理前執行完畢。promise1promise2早於setTimeout輸出,則是由於microtasks永遠在下一個Macrotask啓動前結束。瀏覽器

爲何有些瀏覽器表現不一致

有些瀏覽器的輸出順序爲:script start, script end, setTimeout, promise1, promise2。它們在執行setTimeout後才運行primise的回調函數。這就好像是它們更傾向於將promise的回調函數看作Macrotask的一類。app

這實際上是能夠理解的,promise是來自於ECMAScript而非HTML。ECMAScript擁有一個相似於Macrotask的"jobs"的概念,但這種關係並不能很清晰的區分開vague mailing list discussions。不管如何,更爲廣泛的觀點是,promise是屬於microtask,而且有一些很好的理由。dom

將promise看作Macrotask會致使性能問題,回調函數可能會由於渲染等相關Macrotask產生沒必要要的延後。同時也會致使影響其餘的Macrotask,而且可能打斷和其餘api的交互,並致使其延後。

這裏有個將promise當作microtasks處理的相似說明,an Edge ticket。WebKit內核的作法顯然是正確的,所以我推斷Safari最終也會選擇修復這個問題,同時Firefox43彷佛也已經修復了這個問題。

如何判斷是Macrotask仍是Microtask

直接進行測試是一種辦法。在瀏覽器中直接查看關於promisesetTimeout的輸出,儘管你依賴的實現是正確的。

就像以前所提到的,在ECMAScript中,它們稱microtasks爲「jobs」。在step 8.a of PerformPromiseThen中,EnqueueJob被稱爲添加一個microtask。

如今,讓咱們看一個更復雜的例子。

加入MutationObserver

首先讓咱們寫一段html代碼:

<div class="outer">
  <div class="inner"></div>
</div>

接下來是一段JS:

// 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
     *promise
     *mutate
     *click
     *promise
     *mutate
     *timeout
     *timeout
     */

在不一樣瀏覽器中的表現:
Chrome:
click
promise
mutate
click
promise
mutate
timeout
timeout

FireFox:
click
mutate
click
mutate
timeout
promise
promise
timeout

Safari:
click
mutate
click
mutate
promise
promise
timeout
timeout

Edge:
click
click
mutate
timeout
promise
timeout
promise

哪一個是正確的

拋出‘click’事件的是一個macrotask,Mutation observer 和 promise 的回調函數被當作microtask進行排列。setTimeout的回調會被當作一個 macrotask。

所以Chrome的運行結果纔是正確的。這裏有點奇特的地方反而是microtask在回調函數以後執行(直到沒有其餘的代碼在執行),我認爲這裏是限制了marcotask的完成。這條用於限制回調函數的規則來源自HTML:

If the stack of script settings objects is now empty, perform a microtask checkpoint
HTML: Cleaning up after a callback step 3

同時一個microtask checkpoint遍歷了整個microtask隊列,除非咱們已經在執行microtask隊列。相似的,ECMAScript 描述了jobs:

Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty…(Job能夠在沒有可執行環境和可執行環境的堆爲空的狀況下被初始化)
ECMAScript: Jobs and Job Queues

儘管這裏的「can be」在HTML環境中變成了「must be」。

瀏覽器是怎麼出錯的?

FirefoxSafari在兩次點擊操做之間運行完成了全部的microtasks,就好比mutation的回調函數所展現的,可是promise彷佛有不一樣的排序算法。這是能夠理解的,由於jobs和microtasks之間的聯繫是相對模糊的,但我依然能夠肯定他們會在兩次點擊回調操做之間運行完成。Firefox ticket.Safari ticket.

對於Edge咱們已經能夠肯定它對於promise的隊列類別是不正確的,但它依然在兩次點擊回調操做之間運行完成了全部的microtasks,相反的是它是在調用完成了全部的監聽回調後,兩次點擊操做僅僅觸發了一次mutateBug ticket

試試更復雜的

如今咱們僅僅在代碼最後加入一行新的代碼來取代點擊操做:

// 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);


inner.click();

這將會和上一個例子同樣拋出點擊事件,但咱們使用代碼來取代真實的點擊交互。

試一試

Chrome:
click
click
promise
mutate
promise
timeout
timeout

FireFox:
click
click
mutate
timeout
promise
promise
timeout

Safari:
click
click
mutate
promise
promise
timeout
timeout

Edge:
click
click
mutate
timeout
promise
timeout
promise

爲何會這樣

在全部的監聽回調觸發完成後…

If the stack of script settings objects is now empty, perform a microtask checkpoint
HTML: Cleaning up after a callback step 3

在上一個的例子中,microtasks會在兩個點擊回調之間運行,但.click()使得兩次事件順序同步執行,所以在兩次點擊回調之間依然存在js代碼在運行。而上面的規則確保了microtasks不會打斷正在執行的代碼片斷。這意味着咱們不能在兩次點擊監聽之間執行microtasks隊列,它們將會在監聽回調執行完成後開始運行。

總結

  • Macrotask會順序執行,瀏覽器可能會在其執行間隔中進行渲染操做
  • Microtask會順序執行:

    • 在全部的回調完成以後,且不存在其餘的js代碼正在執行
    • 在每個macrotask完成以後

但願你如今已經清楚了事件循環的相關內容,或者至少能夠去偷個懶休息一下。

相關文章
相關標籤/搜索