摘要:事件循環持續運行,直到清空列隊的任務。在執行期間,瀏覽器可能更新渲染。線索可能會發生多次。由于冒泡,函數再一次執行。這意味著隊列不會在事件回調之間處理,而是在它們之后處理。當觸發成功事件時,相關的對象在事件之后轉為非激活狀態第四步。
一 前言
一直想對異步處理做一個研究,在查閱資料時發現了這篇文章,非常深入的解釋了事件循環中重的任務隊列。原文中有代碼執行工具,強烈建議自己執行一下查看結果,深入體會task執行順序。
建議看這篇譯文之前先看這篇全面講解事件循環的文章:https://mp.weixin.qq.com/s/vI...
翻譯參考了這篇文章的部分內容:https://juejin.im/entry/55dbd...
原文地址:Tasks, microtasks, queues and schedules
當我告訴我的同事 Matt Gaunt 我想寫一篇關于mircrotask、queueing和瀏覽器的Event Loop的文章。他說:“我實話跟你說吧,我是不會看的。” 好吧,無論如何我已經寫完了,那么我們坐下來一起看看,好吧?
如果你更喜歡視頻,Philip Roberts 在 JSConf 上就事件循環有一個很棒的演講——沒有講 microtasks,不過很好的介紹了其它概念。好,繼續!
思考下面 JavaScript 代碼:
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");
控制臺上的輸出順序是怎樣的呢?
Try it正確的答案是:
script start script end promise1 promise2 setTimeout
但是由于瀏覽器實現支持不同導致結果也不一致。
Microsoft Edge, Firefox 40, iOS Safari 及桌面 Safari 8.0.8 在 promise1 和 promise2 之前打印 setTimeout -- 這似乎是瀏覽器廠商相互競爭導致的實現不同。但是很奇怪的是,Firefox 39 和 Safari 8.0.7 竟然結果都是對的(一致的)。
Why this happens要想弄明白這些,你需要知道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 發生了退化,而之前的版本是對的。我在想這是否只是巧合。
How to tell if something uses tasks or microtasks動手試一試是一種辦法,查看相對于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
現在,讓我們看一個更復雜的例子。一個有心的學徒 :“但是他們還沒有準備好”。別管他,你已經準備好了,讓我們開始……
Level 1 bossfight在發出這篇文章之前,我犯過一個錯誤。下面是一段html代碼:
給出下面的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可能會發生多次。
Test it點擊inner區域觸發click事件:
click div.inner :
click promise mutate click promise mutate timeout timeout
click div.outer :
click promise mutate timeout
和你猜想的有不同嗎?如果是,你得到的結果可能也是正確的。不幸的是,瀏覽器實現并不統一,下面是各個瀏覽器下測試結果:
Who"s right?觸發 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.
Level 1 boss"s angry older brother仍然使用上面的例子,假如我們運行下面代碼會怎么樣:
inner.click();
跟之前一樣,它會觸發 click 事件,不過是通過代碼而不是實際的交互動作。
Try it下面是各個瀏覽器的運行情況:
我發誓我一直在Chrome 中得到不同的結果,我已經更新了這個表許許多次了。我覺得我是錯誤地測試了Canary。假如你在 Chrome 中得到了不同的結果,請在評論中告訴我是哪個版本。
Why is it different?這里介紹了它是怎樣發生的:
將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 隊列不會在事件回調之間處理,而是在它們之后處理。
Does any of this matter?重要,它會在偏角處咬你(疼)。我就遇到了這個問題,我在嘗試為IndexedDB創建一個使用promises而不是奇怪的IDBRequest對象的簡單包裝庫時遇到了此問題。它讓 IDB 用起來很有趣。
當 IDB 觸發成功事件時,相關的 transaction 對象在事件之后轉為非激活狀態(第四步)。如果我創建的 promise 在這個事件發生時被resolved,回調應當在第四步之前執行,這時這個對象仍然是激活狀態。但是在 Chrome 之外的瀏覽器中不是這樣,導致這個庫有些無用。
實際上你可以在 Firefox 中解決這個問題,因為 promise polyfills 如 es6-promise 使用 mutation observers 執行回調,它正確地使用了 microtasks。而它在 Safari 下似乎存在競態條件,不過這可能是因為他們糟糕的 IDB 實現。不幸的是 IE/Edge 不一致,因為 mutation 事件不在回調之后處理。
希望不久我們能看到一些互通性。
You made it!總結:
tasks 按序執行,瀏覽器會在 tasks 之間執行渲染。
microtasks 按序執行,在下面情況時執行:
在每個回調之后,只要沒有其它代碼正在運行。
在每個 task 的末尾。
希望你現在明白了事件循環,或者至少得到一個借口出去走一走,躺一躺。
呃,還有人在嗎?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被執行的時機。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/94877.html
摘要:事件循環持續運行,執行列隊。因此一個已解決的調用將立即把一個加入隊列。如上所述,將稱為。這意味著隊列在事件回調之間不處理,而是在它們之后處理。當觸發成功事件時,相關的對象在事件之后轉為非激活狀態第四步。 原文:Tasks, microtasks, queues and schedules git地址:Tasks(任務), microtasks(微任務), queues(隊列) an...
摘要:眾所周知和都屬于上述異步任務的一種那到底為什么和會有順序之分這就是我想分析總結的問題所在了和的作用是為了讓瀏覽器能夠從內部獲取的內容并確保執行棧能夠順序進行。只要執行棧沒有其他在執行,在每個結束時,隊列就會在回調后處理。 前言 我是在做前端面試題中看到了setTimeout和Promise的比較,然后第一次看到了microtask和macrotask的概念,在閱讀了一些文章之后發現沒有...
摘要:本文圍繞瀏覽器的事件循環,而有自己的另一套事件循環機制,不在本文討論范圍。現在我們知道了瀏覽器運行時有一個叫事件循環的機制。將事件循環的當前運行任務設置為。對于相應事件循環的每個環境設置對象通知它們哪些為。 本文圍繞瀏覽器的事件循環,而node.js有自己的另一套事件循環機制,不在本文討論范圍。網上的許多相關技術文章提到了process.nextTick和setImmediate兩個n...
摘要:簡介我把在瀏覽器中運行主要分為以下幾種類型的任務同步任務同步任務是指按照正常順序執行的代碼,比如函數調用,數值運算等等,只要是執行后立即能夠得到結果的就是同步任務。取出微任務隊列中的任務執行,直到隊列被完全清空重復和,直到宏任務隊列被清空。 簡介 ? 我把JavaScript在瀏覽器中運行主要分為以下幾種類型的任務: 同步任務(MainTask) :同步任務是指JavaScr...
摘要:但是導致了很明顯的性能問題。上述兩個例子其實是在這個中找到的,第一個使用的版本是,這個版本的實現是采用了,而后因為的里的有,于是尤雨溪更改了實現,換成了,也就是后一個所使用的。后來尤雨溪了解到是將回調放入的隊列。 結論 對于event loop 可以抽象成一段簡單的代碼表示 for (macroTask of macroTaskQueue) { // 1. Handle cur...
閱讀 1207·2021-11-17 09:33
閱讀 3613·2021-09-28 09:42
閱讀 3343·2021-09-13 10:35
閱讀 2496·2021-09-06 15:00
閱讀 2443·2021-08-27 13:12
閱讀 3612·2021-07-26 23:38
閱讀 1849·2019-08-30 15:55
閱讀 542·2019-08-30 15:53