精讀《Tasks, microtasks, queues and schedules》

黃子毅發表於2020-08-17

1 引言

本週跟著 Tasks, microtasks, queues and schedules 這篇文章一起深入理解這些概念間的區別。

先說結論:

  • Tasks 按順序執行,瀏覽器可能在 Tasks 之間執行渲染。
  • Microtasks 也按順序執行,時機是:

    • 如果沒有執行中的 js 堆疊,則在每個回撥之後。
    • 在每個 task 之後。

2 概述

Event Loop

在說這些概念前,先要介紹 Event Loop。

首先瀏覽器是多執行緒的,每個 JS 指令碼都在單執行緒中執行,每個執行緒都有自己的 Event Loop,同源的所有瀏覽器視窗共享一個 Event Loop 以便通訊。

Event Loop 會持續迴圈的執行所有排隊中的任務,瀏覽器會為這些任務劃分優先順序,按照優先順序來執行,這就會導致 Tasks 與 Microtasks 執行順序與呼叫順序的不同。

promise 與 setTimeout

看下面程式碼的輸出順序:

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,線上程中,同步指令碼執行優先順序最高,然後 promise 任務會存放到 Microtasks,setTimeout 任務會存放到 Tasks,Microtasks 會優先於 Tasks 執行。

Microtasks 中文可以翻譯為微任務,只要有 Microtasks 插入,就會不斷執行 Microtasks 佇列直到結束,在結束前都不會執行到 Tasks。

點選冒泡 + 任務

下面給出了更復雜的例子,提前說明後面的例子 Chrome、Firefox、Safari、Edge 瀏覽器的結果完全不一樣,但只有 Chrome 的執行結果是對的!為什麼 Chrome 是對的呢,請看下面的分析:

<div class="outer">  <div class="inner"></div></div>
// Let's get hold of those elementsvar outer = document.querySelector(".outer");var inner = document.querySelector(".inner");// Let's listen for attribute changes on the// outer elementnew 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 elementsinner.addEventListener("click", onClick);outer.addEventListener("click", onClick);

點選 inner 區塊後,正確輸出順序應該是:

click
promise
mutate
click
promise
mutate
timeout
timeout

邏輯如下:

  1. 點選觸發 onClick 函式入棧。
  2. 立即執行 console.log('click') 列印 click
  3. console.log('timeout') 入棧 Tasks。
  4. console.log('promise') 入棧 microtasks。
  5. outer.setAttribute('data-random') 的觸發導致監聽者 MutationObserver 入棧 microtasks。
  6. onClick 函式執行完畢,此時執行緒呼叫棧為空,開始執行 microtasks 佇列。
  7. 列印 promise,列印 mutate,此時 microtasks 已空。
  8. 執行冒泡機制,outer div 也觸發 onClick 函式,同理,列印 promise,列印 mutate
  9. 都執行完後,執行 Tasks,列印 timeout,列印 timeout

模擬點選冒泡 + 任務

如果將觸發 onClick 行為由點選改為:

inner.click();

結果會不同嗎?答案是會(單元測試與使用者行為不符合,單測也有無解的時候)。然而四大瀏覽器的執行結果也是完全不一樣,但從邏輯上講仍然 Chrome 是對的,讓我們看下 Chrome 的結果:

click
click
promise
mutate
promise
timeout
timeout

邏輯如下:

  1. inner.click() 觸發 onClick 函式入棧。
  2. 立即執行 console.log('click') 列印 click
  3. console.log('timeout') 入棧 Tasks。
  4. console.log('promise') 入棧 microtasks。
  5. outer.setAttribute('data-random') 的觸發導致監聽者 MutationObserver 入棧 microtasks。
  6. 由於冒泡改為 js 呼叫棧執行,所以此時 js 呼叫棧未結束,不會執行 microtasks,反而是繼續執行冒泡,outer 的 onClick 函式入棧。
  7. 立即執行 console.log('click') 列印 click
  8. console.log('timeout') 入棧 Tasks。
  9. console.log('promise') 入棧 microtasks。
  10. MutationObserver 由於還沒呼叫,因此這次 outer.setAttribute('data-random') 的改動實際上沒有作用。
  11. js 呼叫棧執行完畢,開始執行 microtasks,按照入棧順序,列印 promisemutatepromise
  12. microtasks 執行完畢,開始執行 Tasks,列印 timeouttimeout

3 精讀

基於任務排程這麼複雜,且瀏覽器實現方式很不同,下面兩件事是我很不推薦的:

  1. 業務邏輯 “巧妙” 依賴了 microtasks 與 Tasks 執行邏輯的微妙差異。
  2. 死記硬背呼叫順序。

且不說依賴了呼叫順序的業務邏輯本身就很難維護,不同瀏覽器之間對任務呼叫順序還是不同的,這可能源於對 W3C 標準規範理解的偏差,也可能是 BUG,這會導致依賴於此的邏輯非常脆弱。

雖然上面兩個例子非常複雜,但我們也不必把這個例子當作經典背誦,只要記住文章開頭提到的執行邏輯就可以推導:

  • Tasks 按順序執行,瀏覽器可能在 Tasks 之間執行渲染。
  • Microtasks 也按順序執行,時機是:

    • 如果沒有執行中的 js 堆疊,則在每個回撥之後。
    • 在每個 task 之後。

記住 PromiseMicrotaskssetTimeoutTasks,JS 一次 Event Loop 完畢後,即呼叫棧沒有內容時才會執行 Microtasks -> Tasks,在執行 Microtasks 過程中插入的 Microtasks 會按順序繼續執行,而執行 Tasks 中插入的 Microtasks 得等到呼叫棧執行完後才繼續執行。

上面說的內容都是指一次 Event Loop 時立即執行的優先順序,不要和執行延遲時間弄混淆了。

把 JS 執行緒的 Event Loop 當作一個函式,函式內同步邏輯執行優先順序是最高的,如果遇到 MicrotasksTasks 就會立即記錄下來,當一次 Event Loop 執行完後立即呼叫 Microtasks,等 Microtasks 佇列執行完畢後可能進行一些渲染行為,等這些瀏覽器操作完成後,再考慮執行 Tasks 佇列。

4 總結

最後,還是要強調一句,不要依賴 MicrotasksTasks 的執行順序,尤其在申明式程式設計環境中,我們可以把 MicrotasksTasks 都當作是非同步內容,在渲染時做好狀態判斷即可,不用關心先後順序。

討論地址是:精讀《Tasks, microtasks, queues and schedules》· Issue #264 · dt-fe/weekly

如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公眾號

版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

相關文章