一直想對異步處理作一個研究,在查閱資料時發現了這篇文章,很是深刻的解釋了事件循環中重的任務隊列。原文中有代碼執行工具,強烈建議本身執行一下查看結果,深刻體會task執行順序。
建議看這篇譯文以前先看這篇全面講解事件循環的文章:https://mp.weixin.qq.com/s/vI...
翻譯參考了這篇文章的部份內容:https://juejin.im/entry/55dbd...html
原文地址:Tasks, microtasks, queues and schedulesgit
當我告訴個人同事 Matt Gaunt 我想寫一篇關於mircrotask、queueing和瀏覽器的Event Loop的文章。他說:「我實話跟你說吧,我是不會看的。」 好吧,不管如何我已經寫完了,那麼咱們坐下來一塊兒看看,好吧?es6
若是你更喜歡視頻,Philip Roberts 在 JSConf 上就事件循環有一個很棒的演講——沒有講 microtasks,不過很好的介紹了其它概念。好,繼續!github
思考下面 JavaScript 代碼:web
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');
控制檯上的輸出順序是怎樣的呢?chrome
正確的答案是:windows
script start script end promise1 promise2 setTimeout
可是因爲瀏覽器實現支持不一樣致使結果也不一致。api
Microsoft Edge, Firefox 40, iOS Safari 及桌面 Safari 8.0.8 在 promise1 和 promise2 以前打印 setTimeout -- 這彷佛是瀏覽器廠商相互競爭致使的實現不一樣。可是很奇怪的是,Firefox 39 和 Safari 8.0.7 居然結果都是對的(一致的)。promise
要想弄明白這些,你須要知道Event Loop是如何處理 tasks 和 microtasks的。若是你是第一次接觸它,須要花些功夫才能弄明白。深呼吸。。。瀏覽器
每一個線程都有本身的事件循環,因此每一個 web worker 都有本身的事件循環,所以web worker才能夠獨立執行。而來自同域的全部窗口共享一個事件循環,因此它們能夠同步地通訊。事件循環持續運行,直到清空 tasks 列隊的任務。事件循環包括多種任務源,事件循環執行時會訪問這些任務源,這樣就肯定了各個任務源的執行順序(IndexedDB 等規範定義了本身的任務源和執行順序),但瀏覽器能夠在每次循環中選擇從哪一個任務源去執行一個任務。這容許瀏覽器優先考慮性能敏感的任務,例如用戶輸入。Ok ok, 留下來陪我坐會兒……
Tasks 被放到任務源中,瀏覽器內部執行轉移到JavaScript/DOM領域,而且確保這些 tasks按序執行。在tasks執行期間,瀏覽器可能更新渲染。來自鼠標點擊的事件回調須要安排一個task,解析HTML和setTimeout一樣須要。
setTimeout延遲給定的時間,而後爲它的回調安排一個新的task。這就是爲何 setTimeout在 script end 以後打印:script end 在第一個task 內,setTimeout 在另外一個 task 內。好了,咱們快講完了,剩下一點我須要大家堅持下……
Mircotasks隊列一般用於存放一些任務,這些任務應該在正在執行的腳本以後當即執行,好比對一批動做做出反應,或者操做異步執行避免建立整個新任務形成的性能浪費。 只要沒有其餘JavaScript代碼在執行中,而且在每一個task隊列的任務結束時,microtask隊列就會被處理。在處理 microtasks 隊列期間,新添加到 microtasks 隊列的任務也會被執行。 microtasks 包括 MutationObserver callbacks。例如上面的例子中的 promise的callback。
一個settled狀態的promise(直接調用resolve或者reject)或者已經變成settled狀態(異步請求被settled)的promise,會馬上將它的callback(then)放到microtask隊列裏面。這就能保證promise的回調是異步的,即使promise已經變爲settled狀態。所以一個已settled的promise調用.then(yey,nay)時將當即把一個microtask任務加入microtasks任務隊列。這就是爲何 promise1 和 promise2 在 script end 以後打印,由於正在運行的代碼必須在處理 microtasks 以前完成。promise1 和 promise2 在 setTimeout 以前打印,由於 microtasks 老是在下一個 task 以前執行。
好,一步一步的運行:
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); });
沒錯,就是上面這個,我作了一個 step-by-step 動畫圖解。你週六是怎麼過的?和朋友們一塊兒出去玩?我沒有出去。嗯,若是搞不明白個人使人驚歎的UI設計界面,點擊上面的箭頭試試。
瀏覽器實現差別
一些瀏覽器的打印結果:
script start script end setTimeout promise1 promise2
在 setTimeout 以後運行 promise 的回調,就好像將 promise 的回調看成一個新的 task 而不是 microtask。
這多少情有可原,由於 promise 來自 ECMAScript 規範而不是 HTML 規範。ECAMScript 有一個概念 job,和 microtask 類似,可是二者的關係在郵件列表討論中沒有明確。不過,通常共識是 promise 應該是 microtask 隊列的一部分,而且有充足的理由。
將 promise看成task(macrotask)會帶來一些性能問題,由於回調沒有必要由於task相關的事(好比渲染)而延遲執行。與其它 task 來源交互時它也產生不肯定性,也會打斷與其它 API 的交互,不事後面再細說。
我提交了一條 Edge 反饋,它錯誤地將 promises 看成 task。WebKit nightly 作對了,因此我認爲 Safari 最終會修復,而 Firefox 43 彷佛已經修復。
有趣的是 Safari 和 Firefox 發生了退化,而以前的版本是對的。我在想這是否只是巧合。
動手試一試是一種辦法,查看相對於promise和setTimeout如何打印,儘管這取決於實現是否正確。
一種方法是查看規範:
將一個 task 加入隊列: step 14 of setTimeout
將 microtask 加入隊列:step 5 of queuing a mutation record
如上所述,ECMAScript 將 microtask 稱爲 job:
調用 EnqueueJob 將一個 microtask 加入隊列:step 8.a of PerformPromiseThen
如今,讓咱們看一個更復雜的例子。一個有心的學徒 :「可是他們尚未準備好」。別管他,你已經準備好了,讓咱們開始……
在發出這篇文章以前,我犯過一個錯誤。下面是一段html代碼:
<div class="outer"> <div class="inner"></div> </div>
給出下面的JS代碼,若是click div.inner將會打印出什麼呢?
// 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);
繼續,在查看答案以前先試一試。 線索:logs可能會發生屢次。
點擊inner區域觸發click事件:
click div.inner :
click promise mutate click promise mutate timeout timeout
click div.outer :
click promise mutate timeout
和你猜測的有不一樣嗎?若是是,你獲得的結果可能也是正確的。不幸的是,瀏覽器實現並不統一,下面是各個瀏覽器下測試結果:
觸發 click 事件是一個 task,Mutation observer 和 promise 的回調 加入microtask列隊,setTimeout 回調加入task列隊。所以運行過程以下:
點擊內部區域觸發內部區域點擊事件 -> 冒泡到外部區域 -> 觸發外部區域點擊事件 這裏要注意一點: setTimeout 執行時機在冒泡以後,由於也是在microtask以後,準確的說是在最後的時機執行了。
堆棧爲空以後將會執行microtasks裏面的任務。
因爲冒泡, click函數再一次執行。
最後將執行setTimeout。
因此 Chrome 是對的。對我來講新發現是,microtasks 在回調以後運行(只要沒有其它的 Javascript 在運行),我原覺得它只能在一個task 的末尾執行。這個規則來自 HTML 規範,調用一個回調:
If the stack of script settings objects is now empty, perform a microtask checkpoint
— HTML: Cleaning up after a callback step 3
一個 microtask checkpoint 逐個檢查 microtask隊列,除非咱們已經在處理一個 microtask 隊列。相似地,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
儘管在 HTML 中"can be"變成了"must be"。
對於 mutation callbacks,Firefox 和 Safari 都正確地在內部區域和外部區域單擊事件之間執行完畢,清空了microtask 隊列,可是 promises 列隊的處理看起來和chrome不同。這多少情有可原,由於 jobs 和 microtasks 的關係不清楚,可是我仍然指望在事件回調之間處理。Firefox ticket. Safari ticket.
對於 Edge,咱們已經看到它錯誤的將 promises 看成 task,它也沒有在單擊回調之間清空 microtask 隊列,而是在全部單擊回調執行完以後清空,因而總共只有一個 mutate 在兩個 click 以後打印。 Bug ticket.
仍然使用上面的例子,假如咱們運行下面代碼會怎麼樣:
inner.click();
跟以前同樣,它會觸發 click 事件,不過是經過代碼而不是實際的交互動做。
下面是各個瀏覽器的運行狀況:
我發誓我一直在Chrome 中獲得不一樣的結果,我已經更新了這個表許許屢次了。我以爲我是錯誤地測試了Canary。假如你在 Chrome 中獲得了不一樣的結果,請在評論中告訴我是哪一個版本。
這裏介紹了它是怎樣發生的:
將Run srcipt加入Tasks隊列,將inner.click加入執行堆棧:
執行click函數:
按順序執行,分別將setTimeout加入Tasks隊列,將Promise MultationObserver加入microtasks隊列:
click函數執行完畢以後,咱們沒有去處理microtasks隊列的任務,由於此時堆棧不爲空:
咱們不能將 MultationObserver加入microtasks隊列,由於有一個等待處理的 MultationObserver:
如今堆棧爲空了,咱們能夠處理microtasks隊列的任務了:
最終結果:
經過對比事件觸發,咱們要注意兩個地方:JS stack是不是空的決定了microtasks隊列裏任務的執行;microtasks隊列裏不能同時有多個MultationObserver。
正確的順序是:click, click, promise, mutate, promise, timeout, timeout,彷佛 Chrome 是對的。
在每一個listerner callback被調用以後:
If the stack of script settings objects is now empty,perform a microtask checkpoint. — HTML: 回調以後的清理第三步
以前,這意味着 microtasks 在事件回調之間運行,可是如今.click()讓事件同步觸發,所以調用.click()的腳本仍處於回調之間的堆棧中。上面的規則確保了 microtasks 不會中斷正在執行的JS代碼。這意味着 microtasks 隊列不會在事件回調之間處理,而是在它們以後處理。
重要,它會在偏角處咬你(疼)。我就遇到了這個問題,我在嘗試爲IndexedDB建立一個使用promises而不是奇怪的IDBRequest對象的簡單包裝庫時遇到了此問題。它讓 IDB 用起來頗有趣。
當 IDB 觸發成功事件時,相關的 transaction 對象在事件以後轉爲非激活狀態(第四步)。若是我建立的 promise 在這個事件發生時被resolved,回調應當在第四步以前執行,這時這個對象仍然是激活狀態。可是在 Chrome 以外的瀏覽器中不是這樣,致使這個庫有些無用。
實際上你能夠在 Firefox 中解決這個問題,由於 promise polyfills 如 es6-promise 使用 mutation observers 執行回調,它正確地使用了 microtasks。而它在 Safari 下彷佛存在競態條件,不過這多是由於他們糟糕的 IDB 實現。不幸的是 IE/Edge 不一致,由於 mutation 事件不在回調以後處理。
但願不久咱們能看到一些互通性。
總結:
microtasks 按序執行,在下面狀況時執行:
但願你如今明白了事件循環,或者至少獲得一個藉口出去走一走,躺一躺。
呃,還有人在嗎?Hello?Hello?
感謝 Anne van Kesteren, Domenic Denicola, Brian Kardell 和 Matt Gaunt 校對和修正。是的,Matt 最後仍是看了此文,我沒必要把他整成發條橙了。
1.microtask隊列就會被處理的時機
(1)只要沒有其餘JavaScript代碼在執行中, (2)而且在每一個task隊列的任務結束時, microtask隊列就會被處理。
也就是說能夠在執行一個task以後連續執行多個microtask。
2. promise相關
(1)promise一旦建立就會立刻執行 (2)當狀態變爲settled的時候,callback纔會被加入microtask 隊列
因此要注意promise建立和callback被執行的時機。