本文是對於異步系列第一篇裏提到的evenloop
模型中,所提到的任務隊列(task queues)的展開分析html
說明:如下代碼均使用chrome瀏覽器運行 關於瀏覽器表現的差別在最後作補充。web
先看一個典型的例子:chrome
console.log('script start') // 第一個異步任務 setTimeout(()=>{ console.log('setTimeout') },0) // 第二個異步任務 Promise.resolve().then(()=>{ console.log('promise1') }).then(()=>{ console.log('promise2'); }) console.log('script end') // 實際輸出結果: // script start // script end // promise1 // promise2 // setTimeout
根據以前說過的evenloop
模型,首先輸出script start
和script end
沒有什麼問題;
可是接下來卻發現:
先執行了Promise
指定的callback
而不是setTimeout
的callback
。-- Why?api
microtask queue
¯otask queue
)在以前討論evenloop模型時,粗略提到了任務隊列有2種類型:microtask queue
和macrotask queue
,他們的區別在於:promise
macrotask
的執行:是在evenloop的每次循環過程,取出macrotask queue中可執行的第一個(注意不必定是第一個,由於咱們說過例如setTimeout能夠指定任務被執行的最少延遲時間,當前macrotask queue的首位保存的任務可能尚未到執行時間,因此queue只是表明callback
插入的順序,不表明執行時也要按照這個順序)。microtask
的執行:在evenloop的每次循環過程以後,若是當前的執行棧(call stack)爲空,那麼執行microtask queue
中全部可執行的任務 (某些文獻內容中 直接把macrotask
稱爲task
,或者某些中文文章中把它們翻譯成"微任務"和"宏任務",含義都是類似的:macrotask或者task表明相對單獨佔據evenloop過程一次循環的任務,而microtask有可能在一次循環中執行多個)瀏覽器
如今回頭來解析前面的例子:app
script start
setTimeout
,將對應的callback插入macrotask queue
promise
,將對應的callback插入microtask queue
script end
,主函數運行結束,執行棧清空,此時開始檢查microtask queue
,發現裏面有可運行的任務,所以按順序輸出promise1
和promise2
microtask queue
執行完,開始新一輪循環,從macrotask queue
取出setTimeout
任務並執行,輸出setTimeout
常見異步操做對應的回調函數任務類型以下:dom
setTimeout
, setInterval
, setImmediate
, requestAnimationFrame
, I/O
, UI rendering
process.nextTick
, Promises
, Object.observe
, MutationObserver
大概能夠這樣區分:和html交互密切相關的異步操做,通常是macrotasks
;由emcascript
的相關接口返回的異步操做,通常是microtasks
webapp
接下來看一個更復雜的例子,幫助理解不一樣異步任務的執行順序異步
<style> .outer { padding: 30px; background-color: aqua; } .inner { height: 100px; background-color: brown; } </style> <body> <div class="outer">outer <div class="inner">inner</div> </div> </body> <script> 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);
運行以上代碼,能夠在瀏覽器看到兩個嵌套的div(如圖):
點擊inner部分,打開chrome的調試器,能夠看到console打出的結果是:
click
promise
mutate
click
promise
mutate
timeout
timeout
接下來分析運行過程 (建議打開chrome單步調試,進行觀察分析):
inner
,觸發對應的onClick
事件,此時inner對應的onClick
函數進入執行棧;console.log('click')
,輸出(1)click
;setTimeout
,macrotask queue
添加對應的console
函數Promise
,此時microtask queue
添加對應的console
函數outer.setAttribute
,觸發MutationObserver
,microtask queue
添加對應的console
函數(前面註明了MutationObserver建立的回調任務類型是microtask)microtask queue
,所以依次輸出(2)promise
和(3)mutate
,此時當前執行棧call stack
和microtask queue
均爲空,可是macrotask queue
裏依然存儲着兩個東西--inner的Click觸發的任務,以及先前setTimeout的回調函數。 onclick
函數雖然執行完畢,可是因爲事件冒泡
,緊接着要觸發outer
的onClick
的執行函數,所以setTimeout
的回調暫時還沒法執行。outer
的onClick
函數執行過程,重複前面的2-5步驟,所以輸出(4)click
(5)promise
(6)mutate
call stack
和microtask queue
均爲空,macrotask queue
存儲着兩個setTimeout的回調函數。,根據evenloop模型,開始分別執行這兩個task,因而輸出了兩個(7)和(8)timeout
再次建議在調試器查看上面的步驟,尤爲要注意觀察call stack
、microtask queue
macrotask queue
的變化,會更加直觀
在充分理解上面例子的基礎上,咱們把點擊inner部分的這個操做,改爲直接在js代碼的末尾加上innner.click()
,請問結果是否一致呢?
先說最終結果:
click
click
promise
mutate
promise
timeout
timeout
與前一次的結果徹底不一樣!
接下來再次進入調試分析:
inner.click()
,此次進入inner
綁定的onclick
函數時,與前面是有所不一樣的:經過chrome調試器能夠看到,此時的call stack有兩層--除了onClick函數以外,還有一層匿名函數,這層函數其實就是最外層的script,至關於window.onload綁定的處理函數。
這是很關鍵的一點!!!就是這一個區別,致使了整個執行結果的差別。
由於前面的例子的執行順序是:
onclcik
onClick
對應的函數進棧。兩次執行到onclick
時的callstck
區別如圖:
第一次,經過點擊inner
觸發click
:
第二次,經過代碼直接觸發click
接下來分析本次的輸出順序:
2-5
,輸出一個(1)click
inner
的onClick
函數執行完畢,可是此次執行棧並未清空,由於當前匿名函數還在執行棧裏,所以沒法開始調度microtask queue
!!!(前面說過,microtask queue的調度必須在當前執行棧爲空的狀況下),所以,這時候會先進入冒泡事件觸發的onClick
(2)clcik
以後,promise
的回調函數進入microtask queue
outer.setAttribute
,觸發MutationObserver
,可是此時microtask queue
沒法再次添加對應的回調函數了,由於已經有一個存在的監聽函數在pengding
onclick
執行完畢,執行棧清空,接下來開始調度microtask queue
,輸出(3)promise
(4)mutate
(5)promise
call stack
和microtask queue
均爲空,macrotask queue
存儲着兩個setTimeout的回調函數。根據evenloop模型,開始分別執行這兩個task,因而輸出了兩個(6)和(7)timeout
這兩個例子的對比,着重說明了一點:
--microtask queue
存儲的任務,必需要在當前函數執行棧爲空時纔會開始調度。
完整內容可參見html標準中的8.1.4部分
macrotask
會按順序執行,而且有可能被中途插入瀏覽器render
,例如上面的冒泡事件microtask
的執行有兩個條件:
macrotask
結束以後call stack
爲空上述代碼在chrome的瀏覽器下測試結果,可能和在某些版本的firefox和ie瀏覽器下不一致,在某些瀏覽器中可能會把promise
的回調函數當作mascrotask
,可是:
廣泛的共識把 Promise當作是miscrotask
,而且有比較充分的理由:若是把promose當作是task(即mascrotask)將會致使一些性能問題--由於task的調度是能夠被其餘task相關的任務如Render
打斷,還會由於與其餘任務源的交互致使不肯定性。
若是以爲寫得很差/有錯誤/表述不明確,都歡迎指出
若是有幫助,歡迎點贊和收藏,轉載請徵得贊成後著明出處。若是有問題也歡迎私信交流,主頁有郵箱地址
若是以爲做者很辛苦,也歡迎打賞~