Event loop及macrotask & microtask

寫這篇文章的原因有兩個:其一,團隊小夥伴之前分享過《macrotask microtask介紹》這個話題,當時留下了一些疑問,至今仍模棱兩可;其二,看到了「奇舞週刊」轉發了一篇《從 薛定諤的貓 聊到 Event loop》的文章,內容精煉,但是有一些原則性的問題和規範有偏差。
特整理一下相關內容,以免誤導大家,也對自己的掌握做一個總結(大部分內容均來自官方文檔,文檔結尾處有相關鏈接)。

JavaScript 引擎不是單獨運行的 — 它運行在一個宿主環境中,對於大多數開發者來說就是典型的瀏覽器和 Node.js(如今,JavaScript 被應用到了從機器人到燈泡的各種設備上)。每個設備都代表了一種不同類型的 JS 引擎的宿主環境。但,所有的環境都有一個共同點,就是都擁有一個 事件循環 Event Loop 的內置機制,它隨着時間的推移每次都去調用 JavaScript 引擎去處理程序中多個塊的執行。

這意味着 JavaScript 引擎只是 JavaScript 代碼按需執行的環境。是它周圍的環境來調度 JavaScript 代碼執行。
在這裏插入圖片描述
事件循環(Event Loop)的任務很簡單: 監控調用棧和回調隊列。如果調用棧是空的,它就會取出隊列中的第一個事件,然後將它壓入到調用棧中,然後運行它。

Event Loop

Event Loop 是在 HTML Standard 中定義的:To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. Each agent has an associated event loop.

之所以稱之爲事件循環,是因爲它經常按照類似如下的方式來被實現:

while (queue.waitForTask()) {
  queue.processNextTask();
}

如果當前沒有任何任務,queue.waitForTask() 會同步地等待任務到達。

獨立

每個”線程“都有自己的 Event Loop。所以,每個 web worker 擁有獨立的 Event Loop,它們都可以獨立運行;同源的 windows 共享一個 Event Loop,它們之間可以互相通信。

A window event loop is the event loop used by similar-origin window agents. User agents may share an event loop across similar-origin window agents.
A worker event loop is the event loop used by dedicated worker agents, shared worker agents, and service worker agents. There must be one worker event loop per such agent.

執行至完成

每一個任務完整的執行後,其它任務纔會被執行。這爲程序的分析提供了優秀的特性:一個函數執行時,它永遠不會被搶佔,並且在其他代碼運行之前完全運行;與此同時帶來的是,當一個任務需要太長時間才能處理完畢時,Web 應用就無法處理用戶的交互,例如點擊或滾動。

循環過程

Event Loop 期間的某個時刻,運行時從最先進入隊列的消息開始處理隊列中的任務。爲此,這個消息會被移出隊列,並作爲輸入參數調用與之關聯的函數。調用一個函數總是會爲其創造一個新的棧幀 — 見下述「執行棧」描述

函數的處理會一直進行到執行棧再次爲空爲止;然後事件循環將會處理隊列中的下一個任務(如果還有的話)。

在規範的 Processing model 定義了 event loop 的循環過程。 概括來說:

  • Event Loop 會不斷循環的去取 tasks 隊列的中”最老“(最先進入隊列)的一個任務(這裏的任務就是 macrotask )推入棧中執行;並在當次循環裏依次執行並清空 microtask 隊列裏的任務
  • 執行完 microtask 隊列裏的任務,有可能會渲染更新(瀏覽器很聰明,在一幀以內的多次Dom變動瀏覽器不會立即響應,而是會積攢變動以最高60HZ的頻率更新視圖)

永不阻塞

事件循環模型的一個非常有趣的特性是,永不阻塞。 處理 I/O 通常通過事件和回調來執行。

所以,比如當你的 JavaScript 程序發出了一個 Ajax 請求(異步)去服務器獲取數據,在回調函數中寫了相關 response 的處理代碼。 JavaScript 引擎就會告訴宿主環境: “嘿,我現在要暫停執行了,但是當你完成了這個網絡請求,並且獲取到數據的時候,請回來調用這個函數。“然後宿主環境(瀏覽器)設置對網絡響應的監聽,當返回時,它將會把回調函數插入到事件循環隊列裏然後執行。

執行棧

說道 Event Loop,不得不提及執行棧(JavaScript execution context stack),相關官方描述 — Here

JavaScript 是單線程,只有一個執行棧,每一個函數執行的時候,都會生成新的執行上下文(execution context),執行上下文會包含一些當前函數的參數、局部變量之類的信息,它會被推入棧中,正在執行的上下文(running execution context)始終處於棧的頂部。當函數執行完後,它的執行上下文會從棧彈出。

function bar() {
	console.log('bar')
}

function foo() {
	console.log('foo')
	bar()
}

foo()

event_loop_runtime.png
注意: 正在運行的執行上下文(running execution context)始終是此堆棧的頂層元素。每當控制從與當前運行的執行上下文相關聯的可執行代碼轉移到與該執行上下文無關的可執行代碼時,就創建新的執行上下文。新創建的執行上下文被壓入堆棧併成爲正在運行的執行上下文。正在執行的只有一個!!!

====================================== 華麗的分割線 ======================================
至此,我們已經很清晰的知道:Event Loop 從任務隊列獲取任務,然後將任務添加到執行棧中( 動態,根據函數調用),JavaScript 引擎獲取執行棧最頂層元素(即正在運行的執行上下文)進行運行!

那麼,Event Loop 執行過程中,提及到的 macrotask 與 microtask 又有啥區別?

macrotask

An event loop has one or more task queues. A task queue is a set of tasks.

Event Loop 有一個或多個任務隊列,這裏的任務就是文章中提及的 宏任務 — macrotask。

Note:The microtask queue is not a task queue

以下,屬於宏任務(macrotask/task):— Here

  • Events
  • Parsing
  • Callbacks
  • Using a resource(I/O)
  • Reacting to DOM manipulation
  • script
  • setTimeout/setInterval/setImmediate
  • requestAnimationFrame
所以,當 執行棧 爲空時,會從任務隊列裏獲取任務,加入到執行棧中,這裏的任務就是 宏任務

setTimeout 是如何工作的

setTimeout(…) 不會自動的把回調放到事件循環隊列中。它設置了一個定時器,當定時器過期了,宿主環境會將回調放到事件循環隊列中,以便在以後的循環中取走執行它。

setTimeout(myCallback, 1000)

這並不意味着 myCallback 將會在 1000ms 之後執行,而是,在 1000ms 之後將被添加到事件隊列。然而,這個隊列中可能會擁有一些早一點添加進來的事件 — 回調將會等待被執行。這就是我們常說的 setTimeout 不準時的根本原因!

microtask

Each event loop has a microtask queue, which is a queue of microtasks, initially empty. A microtask is a colloquial way of referring to a task that was created via the queue a microtask algorithm. — HTML standard microtask

每個 Event Loop 有一個微任務隊列,同時有一個 microtask checkpoint ,關於 perform-a-microtask-checkpoint 參考 這裏

Event Loop Processing model 中的第 8 步,Microtasks: Perform a microtask checkpoint. 即每執行完成一個宏任務後,就會 check 微任務。

以下,屬於微任務(microtask/jobs):

  • Process.nextTick
  • Promise(aync/await)
  • Object.observe
  • MutationObserver
所以,當某個 宏任務 執行完成後,會先執行 微任務 隊列,執行完成後,再次獲取新的 宏任務。這裏微任務相當於插隊操作!!

ES6 job

HTML standard 中是這樣描述微任務執行時機的:

If the stack of script settings objects is now empty, perform a microtask checkpoint.

而,ES6 中對於 job 的執行是這樣定義的:

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

所以,從描述上看,job 和 microtask 很相似。

完整示例

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

在這裏插入圖片描述

相關規範及參考地址

  • timer initialization steps - 18:Queue the task task
  • queue a mutation record steps - 5:Queue a mutation observer microtask
  • https://html.spec.whatwg.org/multipage/webappapis.html#generic-task-sources
  • https://html.spec.whatwg.org/multipage/webappapis.html#microtask-queue
  • https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
  • https://tc39.es/ecma262/#running-execution-context
  • https://github.com/aooy/blog/issues/5
  • https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章