怎麼理解 microtask & macrotask

雖然是面試常見題,但不要背誦答案,深入探索這個問題的本質,過程其實還蠻有趣的。

realdennis
7 min readNov 5, 2021

Definition

macrotask = task

啊對,其實沒有所謂的 macrotask ,什麼任務、聽起來高大上的專有名詞,macrotask 這詞是為了對比 microtask 而誕生的,而它其實就是指向一般的 task ,什麼是 task 呢? 啊就那些 event timer callback 等等的。

那什麼是 microtask ?

最常見的 microtask 就是 Promise , 當你的 task 裡頭出現了 Promise 的調用,這時 Promise 裡面的 callback 是不會被塞到我們熟知的 task queue,而是呀,其實內部還會有一個優先權更高的 micro task queue。

Pseudo Code — event loop

ref: https://github.com/atotic/event-loop#event-loop-description

我們把目光放在下面的 busy-waiting ,主要幹幾件事

  1. 先 dequeue task
  2. 再 dequeue microtask
  3. rendering

那在 task 處理這些 callback 的時候,若裡面有夾帶 microtask 的 callback 就會被存放在 microtask queue,想當然它的優先權肯定是比一般 callback 高囉。

Comparison case

setTimeout(()=> console.log('timer'),0) 
// -> enqueue in task queue
Promise.resolve().then(()=>console.log('promise'))
// -> enqueue in microtask queue
// output:
// promise (below the task queue done)
// timer (next while iteration)

為什麼是這個順序呢?

/** 
our busy waiting logic...
while(true) {
task = eventLoop.nextTask();
if (task) {
task.execute(); // now we execute above two line
}
eventLoop.executeMicrotasks();
// promise callback, print promise (1)
if (eventLoop.needsRendering())
eventLoop.render();
}

// timer (2) (next while iteration)
**/

講破了好像就跟喝水一樣自然,畢竟常年使用 Promise 在處理非同步請求的 Engineer 可能下意識都有這種概念。

但其實沒有真正釐清兩者的話,總是會有奇妙的例子。

Order inconsistent case

wrap a cacheable fetch (inspired & modified from MDN)

const cache = {};
const myFetch = url => {
if (cache[url]) {
console.log(cache[url])
} else {
fetch(url).then(result => result.arrayBuffer()).then(data => {
cache[url] = data;
console.log(data)
});
}
};
console.log('start');
myFetch('/test123')
console.log('done')

這時一邊是 sync 一邊是 microtask:

cache hit

start
cache result
done

cache miss

start
done
fetch result

好像蠻合理的,但你不會想用到這種 order 異常的函式庫。

這時你可以選擇把上頭的同步工作非同步話,你有很多種選擇:0 ms timer / queueMicrotask / requestAnimationFrame blabla。

但你深知 fetch 後面就是 Promise chain ,這時有看文章的各位就知道可以用 microtask 來封裝一下

const cache = {};
const myFetch = url => {
if (cache[url]) {
Promise.resolve().then(()=>console.log(cache[url]))
} else {
fetch(url).then(result => result.arrayBuffer()).then(data => {
cache[url] = data;
console.log(data)
});
}
};

這樣無論 cache hit or miss , execution order 都會是

start
done
result

還有哪些是 microtask ?

mutationObserver (see WHATWG) 、 queueMicroTask (See MDN)

不過你去看 core-js 在完成 microtask 的時候,其實是彼此互相實作(有什麼用什麼) https://github.com/zloirock/core-js/blob/master/packages/core-js/internals/microtask.js#L41-L57

RequestAnimationFrame 的 callback 跟這兩者的順序?

1. Do the oldest (macro) task
2. Do microtasks
3. If this is a good time to render:
- Do some prep work
- Run requestAnimationFrame callbacks
- Render

其實整篇文章可以參考這份 Spec https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model

不過我們可以看向 8.1.6.3–11.13 在 render 階段他會去把 animationFrames 的 callback 抓出來開始 dequeue,並且把 timestamp 傳進去。

其實這部分跟最上頭貼的 pseudocode 是一致的,可以看一下 render 的 method 。

我覺得整段虛擬碼概括了整個 Event loop 到底發生了什麼。

尾聲

想粗淺的了解 event loop 在瀏覽器的模型是什麼,我推薦看這部影片:

我覺得講的很詳細也很好懂。他的 DEMO 真的生動,你大概就能了解 call stack , 還有 event / timer 到底發生什麼事了。

補充

因為在討論串瞄到,似乎有些誤用文章將 stack and queue 與 call stack / task queue 誤用。

我建議可以回頭學習的基本學科:

  • 資料結構(Chapter: stack & queue / Heap, priority queue)
  • 作業系統(Chapter: Scheduling / Synchronization)

尤其是 OS 的 Scheduling ,或許會對 event loop 感覺豁然開朗。

--

--