什麼是瀏覽器的事件循環(Event Loop)?

本文圍繞瀏覽器的事件循環,而node.js有本身的另外一套事件循環機制,不在本文討論範圍。網上的許多相關技術文章提到了 process.nextTicksetImmediate兩個node.js的API,這裏不予討論。

先看HTML標準的一系列解釋:javascript

爲了協調 事件(event), 用戶交互(user interaction), 腳本(script), 渲染(rendering), 網絡(networking)等,用戶代理(user agent)必須使用 事件循環(event loops)。

有兩類事件循環:一種針對瀏覽上下文(browsing context),還有一種針對worker(web worker)。html

如今咱們知道了瀏覽器運行時有一個叫事件循環的機制。java

一個事件循環有一個或者多個 任務隊列(task queues)。任務隊列是task的有序列表,這些task是如下工做的對應算法:Events,Parsing,Callbacks,Using a resource,Reacting to DOM manipulation。

每個任務都來自一個特定的任務源(task source)。全部來自一個特定任務源而且屬於特定事件循環的任務,一般必須被加入到同一個任務隊列中,可是來自不一樣任務源的任務可能會放在不一樣的任務隊列中。node

舉個例子,用戶代理有一個處理鼠標和鍵盤事件的任務隊列。用戶代理能夠給這個隊列比其餘隊列多3/4的執行時間,以確保交互的響應而不讓其餘任務隊列餓死(starving),而且不會亂序處理任何一個任務隊列的事件。web

每一個事件循環都有一個進入microtask檢查點(performing a microtask checkpoint)的flag標誌,這個標誌初始爲false。它被用來組織反覆調用‘進入microtask檢查點’的算法。算法

總結一下,一個事件循環裏有不少個任務隊列(task queues)來自不一樣任務源,每個任務隊列裏的任務是嚴格按照先進先出的順序執行的,可是不一樣任務隊列的任務的執行順序是不肯定的。按個人理解就是,瀏覽器會本身調度不一樣任務隊列。網上不少文章會提到macrotask這個概念,其實就是指代了標準裏闡述的taskapi

標準同時還提到了microtask的概念,也就是微任務。看一下標準闡述的事件循環的進程模型:promise

  1. 選擇當前要執行的任務隊列,選擇一個最早進入任務隊列的任務,若是沒有任務能夠選擇,則會跳轉至microtask的執行步驟。
  2. 將事件循環的當前運行任務設置爲已選擇的任務。
  3. 運行任務。
  4. 將事件循環的當前運行任務設置爲null。
  5. 將運行完的任務從任務隊列中移除。
  6. microtasks步驟:進入microtask檢查點(performing a microtask checkpoint )。
  7. 更新界面渲染。
  8. 返回第一步。

執行進入microtask檢查點時,用戶代理會執行如下步驟:瀏覽器

  1. 設置進入microtask檢查點的標誌爲true。
  2. 當事件循環的微任務隊列不爲空時:選擇一個最早進入microtask隊列的microtask;設置事件循環的當前運行任務爲已選擇的microtask;運行microtask;設置事件循環的當前運行任務爲null;將運行結束的microtask從microtask隊列中移除。
  3. 對於相應事件循環的每一個環境設置對象(environment settings object),通知它們哪些promise爲rejected。
  4. 清理indexedDB的事務。
  5. 設置進入microtask檢查點的標誌爲false。

如今咱們知道了。在事件循環中,用戶代理會不斷從task隊列中按順序取task執行,每執行完一個task都會檢查microtask隊列是否爲空(執行完一個task的具體標誌是函數執行棧爲空),若是不爲空則會一次性執行完全部microtask。而後再進入下一個循環去task隊列中取下一個task執行...網絡

那麼哪些行爲屬於task或者microtask呢?標準沒有闡述,但各類技術文章總結都以下:

  • macrotasks: script(總體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering
  • microtasks: process.nextTick, Promises, Object.observe(廢棄), MutationObserver

來看一個例子:

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');

(代碼來自Tasks, microtasks, queues and schedules推薦觀看原文的代碼可視化執行步驟

若是你測試的瀏覽器支持的Promise不支持Promise/A+標準,或是你使用了其餘Promise polyfill,運行結果可能有差別。

運行結果是:

script start
script end
promise1
promise2
setTimeout

解釋一下過程。

  1. 一開始task隊列中只有script,則script中全部函數放入函數執行棧執行,代碼按順序執行。

接着遇到了setTimeout,它的做用是0ms後將回調函數放入task隊列中,也就是說這個函數將在下一個事件循環中執行(注意這時候setTimeout執行完畢就返回了)。

  1. 接着遇到了Promise,按照前面所述Promise屬於microtask,因此第一個.then()會放入microtask隊列。
  2. 當全部script代碼執行完畢後,此時函數執行棧爲空。開始檢查microtask隊列,此時隊列不爲空,執行.then()的回調函數輸出'promise1',因爲.then()返回的依然是promise,因此第二個.then()會放入microtask隊列繼續執行,輸出'promise2'。
  3. 此時microtask隊列爲空了,進入下一個事件循環,檢查task隊列發現了setTimeout的回調函數,當即執行回調函數輸出'setTimeout',代碼執行完畢。

繼續看一個更有趣的例子:

HTML代碼:

<div class="outer">
  <div class="inner"></div>
</div>

JavaScript代碼:

// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

// Let's listen for attribute changes on the
// outer element
new 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 elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

(代碼來自Tasks, microtasks, queues and schedules推薦觀看原文的代碼可視化執行步驟
點擊內框後,結果以下:

click
promise
mutate
click
promise
mutate
timeout
timeout

解釋一下過程:
點擊inner輸出'click',Promise和設置outer屬性會依次把Promise和MutationObserver推入microtask隊列,setTimeout則會推入task隊列。此時執行棧爲空,雖而後面還有冒泡觸發,可是此時microtask隊列會先執行,因此依次輸入'promise'和'mutate'。接下來事件冒泡再次觸發事件,過程和開始同樣。接着代碼執行完畢,此時進入下一次事件循環,執行task隊列中的任務,輸出兩個'timeout'。

好了,若是你理解了這個,那麼如今換一下事件觸發的方式。在上面的代碼後面加上

inner.click()

思考看看會有什麼不一樣。

運行結果:

click
click
promise
mutate
promise
timeout
timeout

形成這個差別的結果是什麼呢?由於第一次執行完第一個click事件後函數執行棧並不爲空。
具體代碼運行解釋,能夠查看Tasks, microtasks, queues and schedules

本文參考:
html.spec.whatwg.org
difference-between-javascript-macrotask-and-microtask
Event loop

牆裂建議你們閱讀HTML標準裏闡述的Event Loop,歡迎指正和建議。

相關文章
相關標籤/搜索