Tasks(任務), microtasks(微任務), queues(隊列) and schedules(回調隊列)

若是你更喜歡視頻,Philip Roberts 在 JSConf 上就事件循環有一個很棒的演講——沒有講 microtasks,不過很好的介紹了其它概念。好,繼續!html

試一下

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,可是各瀏覽器不一致。git

Microsoft Edge, Firefox 40, iOS Safari 及桌面 Safari 8.0.8 在 promise1 和 promise2 以前打印 setTimeout ——儘管這彷佛是競爭條件致使的。奇怪的是,Firefox 39 和 Safari 8.0.7 是對的。es6

爲何是這樣的

要想弄明白爲何,你須要知道事件循環如何處理 tasks 和 microtasks。第一次接觸鬚要花些功夫才能弄明白。深呼吸……github

每一個線程都有本身的事件循環,因此每一個 web worker 有本身的事件循環(event loop),因此它能獨立地運行。而全部同源的 window 共享一個事件循環,由於它們能同步的通信。事件循環持續運行,執行 tasks 列隊。一個事件循環有多個 task 來源,而且保證在 task 來源內的執行順序(IndexedDB 等規範定義了本身的 task 來源),在每次循環中瀏覽器要選擇從哪一個來源中選取 task,這使得瀏覽器能優先執行敏感 task,例如用戶輸入。Ok ok, 留下來陪我坐會兒……web

Tasks 被列入隊列,因而瀏覽器能從它的內部轉移到 Javascript/DOM 領地,而且確使這些 tasks 按序執行。在 tasks 之間,瀏覽器能夠更新渲染。來自鼠標點擊的事件回調須要安排一個 task,解析 HTML 和 setTimeout 一樣須要。api

setTimeout 延遲給定的時間,而後爲它的回調安排一個新的 task。這就是爲何 setTimeout 在 script end 以後打印,script end 在第一個 task 內,setTimeout 在另外一個 task 內。好了,咱們快講完了,剩下一點我須要大家堅持下……promise

Mircotasks 一般用於安排一些事,它們應該在正在執行的代碼以後當即發生,例如響應操做,或者讓操做異步執行,以避免付出一個全新 task 的代價。mircotask 隊列在回調以後處理,只要沒有其它執行當中的(mid-execution)代碼;或者在每一個 task 的末尾處理。在處理 microtasks 隊列期間,新添加的 microtasks 添加到隊列的末尾而且也被執行。 microtasks 包括 mutation observer 回調。上面的例子中的 promise 的回調也是。瀏覽器

promise 一旦解決(settled),或者已解決,它便爲它的回調安排一個 microtask。這確使 promise 回調是異步的,即使 promise 已經解決。所以一個已解決的 promise 調用 .then(yey, nay) 將當即把一個 microtask 加入隊列。這就是爲何 promise1 和 promise2 在 script end 以後打印,由於正在運行的代碼必須在處理 microtasks 以前完成。promise1 和 promise2 在 setTimeout 以前打印,由於 microtasks 老是在下一個 task 以前執行。app

好吧,一步一步運行dom

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

(注:原文表示運行結果爲:'script start', 'script end', 'promise1', 'promise2', 'setTimeout')

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

一些瀏覽器的打印結果:script start, script end, setTimeout, promise1, promise2。在 setTimeout 以後運行 promise 的回調,就好像將 promise 的回調看成一個新的 task 而不是 microtask。

這多少情有可原,由於 promise 來自 ECMAScript 規範而不是 HTML 規範。ECAMScript 有一個概念 job,和 microtask 類似,可是二者的關係在郵件列表討論中沒有明確。不過,通常共識是 promise 應該是 microtask 隊列的一部分,而且有充足的理由。

將 promise 看成 task 會致使性能問題,由於回調可能沒必要要地被與 task 相關的事(好比渲染)延遲。與其它 task 來源交互時它也致使不肯定性,也會打斷與其它 API 的交互,不事後面再細說。

我提交了一條 Edge 反饋,它錯誤地將 promises 看成 task。WebKit nightly 作對了,因此我認爲 Safari 最終會修復,而 Firefox 43 彷佛已經修復。

有趣的是 Safari 和 Firefox 發生了退化,而以前的版本是對的。我在想這是否只是巧合。

怎麼判斷是task仍是mictask

測試是一種辦法,查看相對於 promise 和 setTimeout 如何打印,儘管這取決於實現是否正確。

一種方法是查看規範。例如,setTimeout 的第十四步將一個 task 加入隊列,mutation record 的第五步將 microtask 加入隊列。

如上所述,ECMAScript 將 microtask 稱爲 job。PerformPromiseThen 的第八步 調用 EnqueueJob 將一個 microtask 加入隊列。

如今,讓咱們看一個更復雜的例子。(一個有心的學徒 :「可是他們尚未準備好」。別管他,你已經準備好了,讓咱們開始……)

level 1

在寫這篇文章以前我一直 會在這裏出錯。下面是 html 代碼片斷:

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

有以下的 Javascript 代碼,假如我點擊 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);

在給出答案前先實際運行一下吧

測試

點擊裏面的矩形觸發一個 click 事件:

你的猜想是否不一樣?如果,你也多是對的。但不幸的是各瀏覽器不一致:

圖片描述 圖片描述 圖片描述 圖片描述
click click click click
promise mutate click mutate
mutate click mutate click
click mutate timeout mutate
promise timeout promise promise
mutate promise timeout promise
timeout promise promise timeout
timeout timeout timeout

哪一個是正確的

一個 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…

ECMAScript: Jobs and Job Queues
觸發 click 事件是一個 task,Mutation observer 和 promise 回調做爲 microtask 加入列隊,setTimeout 回調做爲 task 加入列隊。所以運行過程以下:

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

因此 Chrome 是對的。對我來講新發現是,microtasks 在回調以後運行(只要沒有其它的 Javascript 在運行,我原覺得它只能在 task 的末尾運行。這個規則來自 HTML 規範,調用一個回調:

If the stack of script settings objects is now empty, perform a microtask checkpoint
— HTML: 回調以後的清理第三步

一個 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…

儘管在 HTML 中"can be"變成了"must be"

其它瀏覽器哪裏錯了了

對於 mutation 回調,Firefox 和 Safari 正確地在單擊回調之間清空 microtask 隊列,可是 promises 列隊彷佛不同。這多少情有可原,由於 jobs 和 microtasks 的關係不清楚,可是我仍然指望在事件回調之間處理。Firefox bugSafari bug

對於 Edge,咱們已經看到它錯誤的將 promises 看成 task,它也沒有在單擊回調之間清空 microtask 隊列,而是在全部單擊回調執行完以後清空,因而總共只有一個 mutate 在兩個 click 以後打印。 Edge bug

level 1 憤怒的老大哥

咱們用一樣的例子運行:

inner.click();

跟以前同樣,它會觸發 click 事件,不過是經過代碼而不是實際的交互動做。

試一下

圖片描述 圖片描述 圖片描述 圖片描述
click click click click
click click click click
promise mutate mutate mutate
mutate timeout timeout promise
promise promise promise promise
timeout promise timeout timeout
timeout timeout promise timeout

我發誓我在 Chrome 中始終獲得不一樣的結果,我更新了這個表許屢次才意識到我測試的是 Canary。假如你在 Chrome 中獲得了不一樣的結果,請在評論中告訴我是哪一個版本。

爲何不一樣

它應該像下面這樣運行:

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

正確的順序是:click, click, promise, mutate, promise, timeout, timeout,彷佛 Chrome 是對的。

在每一個事件回調調用以後:

If the stack of script settings objects is now empty, perform a microtask checkpoint.
— HTML: 回調以後的清理第三步

以前,這意味着 microtasks 在事件回調之間運行,可是 .click() 讓事件同步觸發,因此調用 .click() 的代碼仍然在事件回調之間的棧內。上面的規則確保了 microtasks 不會中斷執行當中的代碼。這意味着 microtasks 隊列在事件回調之間不處理,而是在它們以後處理。

這重要嗎

重要,它會在偏角處咬你(疼)。我就遇到了這個問題,在我嘗試用 promises 而不是用怪異的 IDBRequest 對象爲 IndexedDB 建立一個簡單的包裝庫 時。它讓 IDB 用起來頗有趣

當 IDB 觸發成功事件時,相關的 transaction 對象在事件以後轉爲非激活狀態(第四步)。若是我建立的 promise 在這個事件發生時被履行(resolved),回調應當在第四步以前執行,這時這個對象仍然是激活狀態。可是在 Chrome 以外的瀏覽器中不是這樣,致使這個庫有些無用。

實際上你能夠在 Firefox 中解決這個問題,由於 promise polyfills 如 es6-promise 使用 mutation observers 執行回調,它正確地使用了 microtasks。而它在 Safari 下彷佛存在競態條件,不過這多是由於他們 糟糕的 IDB 實現。不幸的是 IE/Edge 不一致,由於 mutation 事件不在回調以後處理。

但願不久咱們能看到一些互通性。

你作到了!

總結:

  • tasks 按序執行,瀏覽器會在 tasks 之間執行渲染。
  • microtasks 按序執行,在下面狀況時執行:

    • 在每一個回調以後,只要沒有其它代碼正在運行。
    • 在每一個 task 的末尾

但願你如今明白了事件循環,或者至少獲得一個藉口出去走一走躺一躺。

相關文章
相關標籤/搜索