iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 15
5
Modern Web

JavaScript 原力覺醒 - 成為絕地武士之路系列 第 15

JS 原力覺醒 Day15 - Macrotask 與 MicroTask

一路上感謝各位讀者們的支持和回饋。
本 30 天系列文目前已經將篇幅重新整理、編纂成冊。
《JavaScript 概念三明治》在天瓏書局上架囉!
喜歡這個系列,想閱讀更詳細原理說明的讀者可以參考:
https://www.tenlong.com.tw/products/9789864347575

上一篇針對 Promise 的語法做了一個基本的解說,但其實今天的內容才是我想講的,Promise 的運作邏輯不難理解,但若是 Promise 在整個 JS 以及瀏覽器裡的流程可能就比較複雜了,現在我們都知道幾件事情:

  • 一個 Promise 最終會有兩種狀態
  • 對應 Promise 的不同狀態,會各自觸發 .then 與 .catch 兩個函式
  • 利用 Promise 可以達成非同步行為,而且內容可以自訂

而雖然在上一章節一直提到非同步,但是對於 Promise 裡所謂非同步執行的部分,目前我們還是沒有很明確的解釋,到底是哪一部分會以非同步的方式被執行?以及什麼時候會執行?這是這篇文章想要探討跟說明的。

Outline

  • Tasks
  • Micro Tasks
  • Microtask 與 Macrotask 同時發生的例子

Macrotasks

我們在 Event Queue 章節裡面所提到 Web API 有些具有非同步的行為,而在非同步的目的達成之後,瀏覽器會把給定的對應的函式推送到 Event Queue 裡面,這些一個一個函式正好代表每一件要做的事情,因此在 JS 裡面,以「 Task 」或 「Macrotask 」來稱呼,為了避免混淆,以下將用 Macrotask Queue 來指稱之前提到的 Event Queue 。

https://ithelp.ithome.com.tw/upload/images/20190930/20106580UMeCNMZgKH.jpg

關於 Task 有兩個細節可以注意:

  • 以瀏覽器的角度來看,在每一個 Task 結束之前,不會有任何瀏覽器的 rending 產生
  • 如果一個 Task 執行所花的時間過長,那麼瀏覽器就無法執行其他的 Task ,所以過一段時間之後會提出「頁面沒有回應」的警告,建議你關閉這個分頁,這種情況你應該有遇過。

Microtasks

Microtask 通常由 Promise 產生,Promise 裡用到的 .then / .catch 函式會以非同步的方式來被執行,回想下 Queue 的概念,所以的非同步行為指的是,會在全域執行環境執行完之後才被執行,因此一但 Promise 的 callback 內容執行完成,狀態再也不是 pending 時,.then 或 .catch 的函式內容就會被推送到 Queue 裡面等待執行,這個被推送到 Queue 的函式就是 Microtask。

相對於管理 Web API 所屬事件的 Macrotask Queue ,Promise 產生的 Microtask 也有自己的 Queue ,在 JS 內被稱為 Job Queue 或 Microtask Queue,而 Job Queue 與 Event Queue 運作方式上有一點不一樣。

差在哪裡呢?在 Queue 裡面的每個 Macrotask 執行完畢後 ,就算 Event Queue 裡面還有其他的 Task,JS 引擎依舊會優先執行 Microtask Queue 裡面的所有 Task ,在這個同時也不會重新渲染網頁,換句話說,Microtask 的執行是穿插在每個 Macrotask 之間,兩者的差異也就在執行順序的不同而已。

https://ithelp.ithome.com.tw/upload/images/20190930/20106580a7zj27GtsT.jpg

Microtask 與 Macrotask 同時發生的例子

如果還是覺得很抽象,下面我會帶個例子,直接用程式碼來比較 Macrotask 與 Microtask 執行順序的不同,應該比較能夠讓你了解,看看下面的程式碼:

setTimeout(() => alert("timeout"));

Promise.resolve()
  .then(() => alert("promise"));

alert("global ex. context");

這段程式碼剛好同時用到 Web API 與 Promise ,各自在呼叫後會產生一個 Macrotask 以及 Microtask ,不過在順序上是哪個會先被執行呢?稍微分析一下:

  • 所有的 Queue 都會等待執行環境堆疊被清空,alert 肯定會先執行
  • setTimeout 對應的函式會被當作一個 Macrotask ,等待時間到之後被送入 Macrotask Queue
  • Promise 對應的 .then 或 .catch 的函式會被當作一個 Microtask 送入 Microtask Queue
  • 在執行環境堆疊清空之後,通常網頁會先做一次 Render,Render 的動作同時也算是一個 Macrotask

因此推測 alert 的順序應該會像是這樣:

  1. "global ex. contenxt"
  2. "timeout"
  3. "promise"

但是並不是!結果會是 "promise""timeout" 還要更先被 log 出來:

  1. "global ex. contenxt"
  2. "promise"
  3. "timeout"

這是為什麼呢?這邊可能會有點抽象,前面我們在分析 JS 語法與運作模式的時候,大多是從 JS 引擎的角度出發。而前面也有提到, Queue 的概念並不屬於 JS 引擎的一部分,相對的歸屬於瀏覽器。對於瀏覽器來說,在網頁頁面開啟時,載入對應的 JS 檔並且執行這件事情,也是一個 Macrotask 。

而剛剛提到 Macrotask 執行完畢後,會優先執行 Microtask ,因此你會看到 "promise" 出現的順序先於 "timeout"

https://ithelp.ithome.com.tw/upload/images/20190930/20106580BZxnDVGnKD.jpg


上一篇
JS 原力覺醒 Day14 - 一生懸命的約定:Promise
下一篇
JS 原力覺醒 Day16 - Async / Await:Promise 語法糖
系列文
JavaScript 原力覺醒 - 成為絕地武士之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
Nono
iT邦新手 5 級 ‧ 2019-10-08 11:51:08
setTimeout(() => alert("timeout"));

Promise.resolve()
  .then(() => alert("promise"));

alert("global ex. context");

這個執行的結果有筆誤打錯嗎?

看更多先前的回應...收起先前的回應...
Spark iT邦新手 5 級 ‧ 2019-10-08 12:06:24 檢舉

Hi, Nono :
很抱歉是我筆誤了,這邊是想表達在渲染完之後會先執行 MicroTask ,所以 Promise 的內容會先被執行!非常感謝你的回復,看來我整個系列寫完之後要再仔細校稿一次...

Nono iT邦新手 5 級 ‧ 2019-10-08 18:07:53 檢舉

最近也剛好在研究 event loop 相關的資料。

普遍找到的資料都是說再一次 event loop 循環中

  1. 會先從 task queue 中,找出一個最優先被加入的 macrotask (oldest task),並放進 event loop。若 task queue 中沒有 macrotask 存在,則直接跳過 2. 到步驟 3.
  2. 執行 macrotask
  3. macrotask 執行完後,此時 execution context stack 應該要是空的,才執行 microtask
  4. 執行 microtask 之中,若又有新的 microtask 被產生,則回到 3. 執行到 microtask queue 為空為止
  5. microtask 執行完才開始做頁面的渲染 (不一定每次都做)
  6. 重複 1-5

依照上面的模型來跑

setTimeout(() => alert("timeout"));

Promise.resolve()
  .then(() => alert("promise"));

alert("global ex. context");
  1. 發現 setTimeout,因為發現是 delay time 為 0,所以把他的 callback 直接放入 task queue 中
  2. Promise.resolve() 會產生 microtask,放入 microtask queue 中
  3. 執行 alert
  4. 此 task 執行完畢,發現有 microtask queue 中有 microtask,且此時 execution context stack 也為空,執行 promise 的內容
  5. 確認 micro queue 為空後,開始做頁面的重繪 (此次 event loop 結束)
  6. 再來是新的一圈,發現 task queue 中有 macrotask,所以拿出來執行 setTimeout 的 callback

感覺和作者畫的最後那張圖不太一樣

以上是個人最近研究的一些心得,有任何錯誤請見諒

附上一些參考資料
asks, microtasks, queues and schedules
whatwg - Event loop processing model

Spark iT邦新手 5 級 ‧ 2019-10-08 23:15:30 檢舉

Hi , Nono :
基本上我的理解跟你寫的五步驟是差不多的,所以只要了解幾個重點:

  1. 在每個 macroTask 執行完之後會執行 microTask
  2. 當有microTask 執行時,任何渲染作業必須等到 MicroTask queue 清空後才執行,而且microTask 會一直堆疊,只要有 microTask 一直產生,頁面就會被迫停著等待,所以使用 Promise 時要小心
  3. 我找到的文獻是說:頁面載入要執行 JS 的 script tag 這件事情,瀏覽器將之視為一個 Macro task ,附上當時參考的文章:
    https://blog.bitsrc.io/microtask-and-macrotask-a-hands-on-approach-5d77050e2168

At the execution of any JS file, the JS engine wraps the contents in a function and associates the function with an event either start or launch. The JS engine emits the start event, the events are added to the task queue (as a macrotask).

而為什麼你會覺得有問題的差別大概就在第三點吧,因為我想強調這點,才把它放進堆疊圖內,但是可能 Event Loop 的形狀太像是迴圈了,可能因此容易誤導執行順序,不知道你是不是因為這樣才覺得疑惑,如果是的話,我到時候可能要回頭重新想一下怎麼呈現這裡的概念模型,先感謝你的討論!

Nono iT邦新手 5 級 ‧ 2019-10-09 14:20:06 檢舉

Hi, Mooji:
非常感謝你的回覆!
那這樣我覺得應該是圖片上的理解有點會讓人誤會
附上之前找到的一張還不錯的圖給你做參考
event loop

你提供的這篇文章滿有趣的還自己用 js 做範例!

最後能偷偷問一下你的圖怎麼做的嗎XD

感謝你百忙之中回覆我
鐵人賽辛苦啦!剩沒幾天了~加油!

Spark iT邦新手 5 級 ‧ 2020-01-05 15:40:13 檢舉

Hi Nono:
抱歉剛剛發現之前好像忘記回你XD
我那個時候是用 Sketch 來畫的,
雖來他原來的功能是用來做UI介面的,
但是我發現也很適合用來畫概念圖,就拿來試試看了,
看來成效還不錯!

Hi Nono,請問4. 此 task 執行完畢是指什麼task? 模型 step2的執行 macrotask好像底下沒有跑到?

1
ceall8650
iT邦新手 5 級 ‧ 2021-03-30 01:28:25

Hi 您好
最近我在研究Event Loop 的機制
但有一點不太明白,
很多文件說明在Event Loop的第一步是:
"從macrotask queue裡面取出oldest task放到event loop內執行"
然後要在這個task執行完後 才會將所有的microtask 執行完畢
最後再將其他macrotask執行完畢

但是我目前測試的結果 都會是先印出microtask的結果 再印出剩下的macrotask的結果
例如這個測試 https://jsfiddle.net/9k153st8/

這樣跟上面的流程好像有點不太一樣

不知道是哪個地方我有認知錯誤
還是哪個流程有誤
能否麻煩您在幫我指正一下
謝謝

看更多先前的回應...收起先前的回應...
Spark iT邦新手 5 級 ‧ 2021-03-30 07:53:59 檢舉

嗨ceall8650:
你這個問題我也有疑惑過,
後來我找到的一個比較普遍的解釋方式是,
對瀏覽器內的 Event Loop 來說,第一次載入 JS 來執行這個動作,也是一個 MacroTask ,所以在全域環境的所有堆疊結束之後,它會直接執行 MicoTask 裡面的 Task。

可以參考:
https://stackoverflow.com/questions/52019729/why-is-this-microtask-executed-before-macrotask-in-event-loop

或是範例比較複雜的這篇:
https://www.linkedin.com/pulse/javascript-under-hood-microtasks-macrotasks-eliran-elnasi/?trk=read_related_article-card_title

不知道這樣有沒有解答到你的問題?有什麼新的結論也可以再跟我說!

ceall8650 iT邦新手 5 級 ‧ 2021-04-01 09:44:32 檢舉

Mooji 感謝您的回覆與分享
我有再看了一下, 應該是原本的JS也算是external script 的一種沒錯, 所以算是macrotask.
而之前搞混的原因主要是認為 "microtask會比macrotask執行" 的原因, 所以在event loop 時, macrotask應該是最後一個步驟. 但我認為應該是microtask 是最後一個步驟, 如果microtask執行完後 macrotask queue仍有task, 應該是開始下一個loop. 也就符合Event Loop 執行的順序

以上是我的看法, 看這樣理解你覺得是否合理

Spark iT邦新手 5 級 ‧ 2021-04-12 23:14:53 檢舉

你說的最後一個步驟是什麼意思?應該沒有所謂真正的最後一個步驟,只有誰會比誰會先被執行的問題,基本上搞清楚這個順序應該就可以解答你的問題

ceall8650 iT邦新手 5 級 ‧ 2021-04-15 01:23:41 檢舉

恩...我這邊的步驟指的是每一次Loop的時候, 會做的哪些步驟.
我是這邊是參考這兩個的地方來幫助我理解
https://javascript.info/event-loop#summary
https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model
我覺得在定義"loop"的時候應該也會有它的意義, 所以我是認為應該會有"第一步"跟"最後一步", 然後再重新回到第一步, 形成一個loop. 或許這樣比較符合這樣的意思.
以上是我自己的解讀跟看法

感謝您撥空回覆

我要留言

立即登入留言