一文搞懂EventLoop與堆棧,宿主環境之間的事件環機制

參考文章Tasks, microtasks, queues and schedulesjavascript

事件循環機制 Event Loop

首先讓咱們來思考這樣的一段代碼html

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');
複製代碼

相信不少程序員都能準確的答出執行順序,若是不知道,test it!java

那麼爲何會這樣呢?

要了解這一點,咱們必須理解事件循環如何處理任務和微任務程序員

ES6 規範中,microtask 稱爲 jobsmacrotask 稱爲 task, 本文中 task 指宏任務, microtask 指微任務。promise

js 引擎是單線程運行的,每一個「線程」都有本身的事件循環,所以每一個Web工做者都有本身的事件循環,所以能夠獨立執行,而同一源上的全部窗口均可以共享事件循環,由於它們能夠同步通訊。 事件循環持續運行,執行全部排隊的任務。 事件循環具備多個任務源,這些任務源保證了該源中的執行順序,但瀏覽器能夠在循環的每一個循環中選擇從哪一個源執行任務。 這個選擇的實現就是咱們的 事件循環機制核心。瀏覽器

計劃了任務,以便瀏覽器能夠從其內部切換 JavaScript / DOM領域,並確保這些操做順序發生。 在任務之間,瀏覽器能夠呈現更新。 從鼠標單擊進入事件回調須要安排任務,解析HTML也是這樣,在上面的示例中是setTimeout。markdown

setTimeout等待給定的延遲,而後爲其回調安排新任務。 這就是爲何在腳本結束以後打印 setTimeout 的緣由,由於打印 script end 是第一個 task 的一部分,而setTimeout被記錄在單獨的 task 中。瀏覽器線程在兩個 task 之間執行 DOM 的更新渲染等宿主環境須要執行的任務。app

到這一步總結就是宿主環境在運行時的順序是:dom

  • task ==> 宿主環境任務 ==> task ==> 宿主環境任務 ==> done

一般,微任務是當前正在執行的 task 發生的事情安排的,例如對一批動做作出反應,或使某些事情異步而不做爲一個新 task ,僅僅做爲一個小的反作用被立馬實現。 只要當前 task 沒有其餘 JavaScript 在執行,微任務隊列就會在回調以後進行處理,也就是在每一個任務結束時進行處理。 在微任務期間排隊的任何其餘微任務都將添加到隊列的末尾並進行處理。 微任務包括變異觀察者回調 MutaionObserverpromise 回調, 以及 process.nextTick(Node.js)異步

一旦 promise 得以解決,或者若是 promise 已經解決,它就會將微任務排隊以進行反動回調。 這樣能夠確保即便promise已經解決,promise回調也是異步的。 所以,針對已解決的Promise調用.then(yey,nay)會當即將微任務排隊。 這就是爲何在腳本結束後記錄 promise1promise2 的緣由,由於當前運行的腳本必須在處理微任務以前完成。 由於微任務老是在下一個任務以前發生,因此 promise1promise2setTimeout 以前記錄

到這一步總結就是宿主環境在運行時的順序是:

  • task ==> microtask ==> 宿主環境任務 ==> task ==> microtask ==> 宿主環境任務 ==> done

老是在下一個 task 以前把 上一個任務的反作用 microtask 執行完畢,而後執行宿主環境任務後開始下一個 task

一些瀏覽器的不同的處理

部分瀏覽器會打印 script start, script end, setTimeout, promise1, promise2.

他們在 setTimeout 以後運行 promise 回調。 他們可能將 promise 回調稱爲 task 的一部分,而不是微任務 microtask

promise 做爲 task 的一部分會致使性能問題,由於回調可能會因與任務相關的宿主任務(例如渲染)而沒必要要地延遲。 它還會因爲與其餘任務源的交互而致使不肯定性,並可能中斷與其餘API的交互。

ECMAScript具備相似於微任務的「做業」概念,廣泛的共識是,應將 promise 做爲微任務隊列的一部分,這是有充分理由的。WebKit 內核始終都能正確的處理任務之間的關係。

一個更復雜的例子

讓咱們來建立一個 HTML 文檔,並思考一下的代碼。

<html>

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>Untitled Document</title>
</head>

<body>
  <div class="outer" style="width: 200px;height: 200px;background: #888888;">
    <div class="inner" style="width: 160px;height: 160px;background: #444444;"></div>
  </div>
</body>

<script> // 獲取兩個元素 var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); // 監聽 outer 的元素屬性變化 new MutationObserver(function () { console.log('mutate'); }).observe(outer, { attributes: true, }); // 點擊回調 function onClick() { console.log('click'); setTimeout(function () { console.log('timeout'); }, 0); Promise.resolve().then(function () { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } inner.addEventListener('click', onClick); outer.addEventListener('click', onClick); </script>

</html>
複製代碼

結合上面對 taskmicrotask 的分析,咱們很容易做出如下分析:

  • 點擊回調是一項 task
  • promiseMutationObserver 回調做爲 microtask 排隊
  • setTimeout 做爲一個新的 task 放到隊列中。

因此是這麼回事

// click
// promise
// mutate
// click
// promise
// mutate
// timeout
// timeout
複製代碼

這裏須要注意的,實際在於微任務在點擊回調以後被處理,

這來自於 HTML 的規範

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

— HTML: Cleaning up after a callback step 3

若是腳本執行堆棧被設置爲空,請執行微任務檢查點

一樣,ECMAScript對做業 jobs (ECMAScript 將微任務稱之爲 jobs )進行說明:

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

僅當沒有正在運行的執行上下文而且執行上下文堆棧爲空時才能夠啓動做業的執行。

這裏的點擊回調是瀏覽器處理的,實際被宿主環境在某個循環結束的時候,做爲下一個 task 被推入堆棧執行。因此執行回調完畢,即出棧。進行對應微任務檢查點。

有興趣可使用瀏覽器控制檯 debugger 回調執行時,堆棧只有 回調執行的堆棧。不存在其餘執行堆棧。因此點擊回調屬於直接被宿主瀏覽器推入堆棧執行。

這與 ECMAScript 的規範上下文堆棧爲空時才能被執行相呼應。

那麼讓咱們作點別的事情

在上面測試代碼的 script 最後加入代碼觸發點擊事件。並思考會有什麼樣的變化。

inner.click()
複製代碼

那麼能夠看到的是執行輸出:

// click
// click
// promise
// mutate
// promise
// timeout
// timeout
複製代碼

根據上面提到的 HTML 和 ECMAScript 規範對 執行堆棧與微任務執行的描述

之前,微任務在偵聽器回調之間運行,可是 .click() 致使事件同步分派,調用 .click() 的腳本仍在回調之間的堆棧中。 上面的規則確保微任務不會中斷執行中的 JavaScript 。 這意味着咱們不在偵聽器回調之間處理微任務隊列,而是在兩個偵聽器以後對它們進行處理。咱們能夠理解爲,由於 .click() 的堆棧保留,致使宿主環境把兩次的回調,做爲一次 task 執行。

總結

  • 任務按順序執行,瀏覽器能夠在它們之間進行渲染
  • 微任務按順序執行,並:
    • 在每次回調以後,只要沒有其餘JavaScript在執行中間
    • 在每一個任務結束時

流程分析

task ==> clear call stack ==> microtask ==> 宿主環境任務 ==> task

相關文章
相關標籤/搜索