參考文章Tasks, microtasks, queues and schedulesjavascript
首先讓咱們來思考這樣的一段代碼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
稱爲jobs
,macrotask
稱爲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
在執行,微任務隊列就會在回調以後進行處理,也就是在每一個任務結束時進行處理。 在微任務期間排隊的任何其餘微任務都將添加到隊列的末尾並進行處理。 微任務包括變異觀察者回調 MutaionObserver
, promise
回調, 以及 process.nextTick(Node.js)
。異步
一旦 promise
得以解決,或者若是 promise
已經解決,它就會將微任務排隊以進行反動回調。 這樣能夠確保即便promise已經解決,promise回調也是異步的。 所以,針對已解決的Promise調用.then(yey,nay)會當即將微任務排隊。 這就是爲何在腳本結束後記錄 promise1
和 promise2
的緣由,由於當前運行的腳本必須在處理微任務以前完成。 由於微任務老是在下一個任務以前發生,因此 promise1
和 promise2
在 setTimeout
以前記錄
到這一步總結就是宿主環境在運行時的順序是:
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>
複製代碼
結合上面對 task
和 microtask
的分析,咱們很容易做出如下分析:
task
promise
和 MutationObserver
回調做爲 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
執行。
流程分析
task
==> clear call stack
==> microtask
==> 宿主環境任務
==> task