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
邏輯如下:
- 點選觸發
onClick
函式入棧。 - 立即執行
console.log('click')
列印click
。 console.log('timeout')
入棧 Tasks。console.log('promise')
入棧 microtasks。outer.setAttribute('data-random')
的觸發導致監聽者MutationObserver
入棧 microtasks。onClick
函式執行完畢,此時執行緒呼叫棧為空,開始執行 microtasks 佇列。- 列印
promise
,列印mutate
,此時 microtasks 已空。 - 執行冒泡機制,outer div 也觸發
onClick
函式,同理,列印promise
,列印mutate
。 - 都執行完後,執行 Tasks,列印
timeout
,列印timeout
。
模擬點選冒泡 + 任務
如果將觸發 onClick
行為由點選改為:
inner.click();
結果會不同嗎?答案是會(單元測試與使用者行為不符合,單測也有無解的時候)。然而四大瀏覽器的執行結果也是完全不一樣,但從邏輯上講仍然 Chrome 是對的,讓我們看下 Chrome 的結果:
click
click
promise
mutate
promise
timeout
timeout
邏輯如下:
inner.click()
觸發onClick
函式入棧。- 立即執行
console.log('click')
列印click
。 console.log('timeout')
入棧 Tasks。console.log('promise')
入棧 microtasks。outer.setAttribute('data-random')
的觸發導致監聽者MutationObserver
入棧 microtasks。- 由於冒泡改為 js 呼叫棧執行,所以此時 js 呼叫棧未結束,不會執行 microtasks,反而是繼續執行冒泡,outer 的
onClick
函式入棧。 - 立即執行
console.log('click')
列印click
。 console.log('timeout')
入棧 Tasks。console.log('promise')
入棧 microtasks。MutationObserver
由於還沒呼叫,因此這次outer.setAttribute('data-random')
的改動實際上沒有作用。- js 呼叫棧執行完畢,開始執行 microtasks,按照入棧順序,列印
promise
,mutate
,promise
。 - microtasks 執行完畢,開始執行 Tasks,列印
timeout
,timeout
。
3 精讀
基於任務排程這麼複雜,且瀏覽器實現方式很不同,下面兩件事是我很不推薦的:
- 業務邏輯 “巧妙” 依賴了 microtasks 與 Tasks 執行邏輯的微妙差異。
- 死記硬背呼叫順序。
且不說依賴了呼叫順序的業務邏輯本身就很難維護,不同瀏覽器之間對任務呼叫順序還是不同的,這可能源於對 W3C 標準規範理解的偏差,也可能是 BUG,這會導致依賴於此的邏輯非常脆弱。
雖然上面兩個例子非常複雜,但我們也不必把這個例子當作經典背誦,只要記住文章開頭提到的執行邏輯就可以推導:
- Tasks 按順序執行,瀏覽器可能在 Tasks 之間執行渲染。
Microtasks 也按順序執行,時機是:
- 如果沒有執行中的 js 堆疊,則在每個回撥之後。
- 在每個 task 之後。
記住 Promise
是 Microtasks
,setTimeout
是 Tasks
,JS 一次 Event Loop 完畢後,即呼叫棧沒有內容時才會執行 Microtasks
-> Tasks
,在執行 Microtasks
過程中插入的 Microtasks
會按順序繼續執行,而執行 Tasks
中插入的 Microtasks
得等到呼叫棧執行完後才繼續執行。
上面說的內容都是指一次 Event Loop 時立即執行的優先順序,不要和執行延遲時間弄混淆了。
把 JS 執行緒的 Event Loop 當作一個函式,函式內同步邏輯執行優先順序是最高的,如果遇到 Microtasks
或 Tasks
就會立即記錄下來,當一次 Event Loop 執行完後立即呼叫 Microtasks
,等 Microtasks
佇列執行完畢後可能進行一些渲染行為,等這些瀏覽器操作完成後,再考慮執行 Tasks
佇列。
4 總結
最後,還是要強調一句,不要依賴 Microtasks
與 Tasks
的執行順序,尤其在申明式程式設計環境中,我們可以把 Microtasks
與 Tasks
都當作是非同步內容,在渲染時做好狀態判斷即可,不用關心先後順序。
討論地址是:精讀《Tasks, microtasks, queues and schedules》· Issue #264 · dt-fe/weekly
如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公眾號
版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)