本文圍繞瀏覽器的事件循環,而node.js有本身的另外一套事件循環機制,不在本文討論範圍。網上的許多相關技術文章提到了process.nextTick
和setImmediate
兩個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
這個概念,其實就是指代了標準裏闡述的task
。api
標準同時還提到了microtask
的概念,也就是微任務。看一下標準闡述的事件循環的進程模型:promise
- 選擇當前要執行的任務隊列,選擇一個最早進入任務隊列的任務,若是沒有任務能夠選擇,則會跳轉至microtask的執行步驟。
- 將事件循環的當前運行任務設置爲已選擇的任務。
- 運行任務。
- 將事件循環的當前運行任務設置爲null。
- 將運行完的任務從任務隊列中移除。
- microtasks步驟:進入microtask檢查點(performing a microtask checkpoint )。
- 更新界面渲染。
- 返回第一步。
執行進入microtask檢查點時,用戶代理會執行如下步驟:瀏覽器
- 設置進入microtask檢查點的標誌爲true。
- 當事件循環的微任務隊列不爲空時:選擇一個最早進入microtask隊列的microtask;設置事件循環的當前運行任務爲已選擇的microtask;運行microtask;設置事件循環的當前運行任務爲null;將運行結束的microtask從microtask隊列中移除。
- 對於相應事件循環的每一個環境設置對象(environment settings object),通知它們哪些promise爲rejected。
- 清理indexedDB的事務。
- 設置進入microtask檢查點的標誌爲false。
如今咱們知道了。在事件循環中,用戶代理會不斷從task隊列中按順序取task執行,每執行完一個task都會檢查microtask隊列是否爲空(執行完一個task的具體標誌是函數執行棧爲空),若是不爲空則會一次性執行完全部microtask。而後再進入下一個循環去task隊列中取下一個task執行...網絡
那麼哪些行爲屬於task或者microtask呢?標準沒有闡述,但各類技術文章總結都以下:
macrotasks
: script(總體代碼), setTimeout, setInterval, setImmediate, I/O, UI renderingmicrotasks
: 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
解釋一下過程。
接着遇到了setTimeout
,它的做用是0ms後將回調函數放入task隊列中,也就是說這個函數將在下一個事件循環中執行(注意這時候setTimeout執行完畢就返回了)。
Promise
,按照前面所述Promise屬於microtask,因此第一個.then()會放入microtask隊列。繼續看一個更有趣的例子:
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,歡迎指正和建議。