摘要:事件循環(huán)持續(xù)運(yùn)行,直到清空列隊(duì)的任務(wù)。在執(zhí)行期間,瀏覽器可能更新渲染。線索可能會(huì)發(fā)生多次。由于冒泡,函數(shù)再一次執(zhí)行。這意味著隊(duì)列不會(huì)在事件回調(diào)之間處理,而是在它們之后處理。當(dāng)觸發(fā)成功事件時(shí),相關(guān)的對(duì)象在事件之后轉(zhuǎn)為非激活狀態(tài)第四步。
一 前言
一直想對(duì)異步處理做一個(gè)研究,在查閱資料時(shí)發(fā)現(xiàn)了這篇文章,非常深入的解釋了事件循環(huán)中重的任務(wù)隊(duì)列。原文中有代碼執(zhí)行工具,強(qiáng)烈建議自己執(zhí)行一下查看結(jié)果,深入體會(huì)task執(zhí)行順序。
建議看這篇譯文之前先看這篇全面講解事件循環(huán)的文章:https://mp.weixin.qq.com/s/vI...
翻譯參考了這篇文章的部分內(nèi)容:https://juejin.im/entry/55dbd...
原文地址:Tasks, microtasks, queues and schedules
當(dāng)我告訴我的同事 Matt Gaunt 我想寫一篇關(guān)于mircrotask、queueing和瀏覽器的Event Loop的文章。他說:“我實(shí)話跟你說吧,我是不會(huì)看的?!?好吧,無(wú)論如何我已經(jīng)寫完了,那么我們坐下來(lái)一起看看,好吧?
如果你更喜歡視頻,Philip Roberts 在 JSConf 上就事件循環(huán)有一個(gè)很棒的演講——沒有講 microtasks,不過很好的介紹了其它概念。好,繼續(xù)!
思考下面 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");
控制臺(tái)上的輸出順序是怎樣的呢?
Try it正確的答案是:
script start script end promise1 promise2 setTimeout
但是由于瀏覽器實(shí)現(xiàn)支持不同導(dǎo)致結(jié)果也不一致。
Microsoft Edge, Firefox 40, iOS Safari 及桌面 Safari 8.0.8 在 promise1 和 promise2 之前打印 setTimeout -- 這似乎是瀏覽器廠商相互競(jìng)爭(zhēng)導(dǎo)致的實(shí)現(xiàn)不同。但是很奇怪的是,F(xiàn)irefox 39 和 Safari 8.0.7 竟然結(jié)果都是對(duì)的(一致的)。
Why this happens要想弄明白這些,你需要知道Event Loop是如何處理 tasks 和 microtasks的。如果你是第一次接觸它,需要花些功夫才能弄明白。深呼吸。。。
每個(gè)線程都有自己的事件循環(huán),所以每個(gè) web worker 都有自己的事件循環(huán),因此web worker才可以獨(dú)立執(zhí)行。而來(lái)自同域的所有窗口共享一個(gè)事件循環(huán),所以它們可以同步地通信。事件循環(huán)持續(xù)運(yùn)行,直到清空 tasks 列隊(duì)的任務(wù)。事件循環(huán)包括多種任務(wù)源,事件循環(huán)執(zhí)行時(shí)會(huì)訪問這些任務(wù)源,這樣就確定了各個(gè)任務(wù)源的執(zhí)行順序(IndexedDB 等規(guī)范定義了自己的任務(wù)源和執(zhí)行順序),但瀏覽器可以在每次循環(huán)中選擇從哪個(gè)任務(wù)源去執(zhí)行一個(gè)任務(wù)。這允許瀏覽器優(yōu)先考慮性能敏感的任務(wù),例如用戶輸入。Ok ok, 留下來(lái)陪我坐會(huì)兒……
Tasks 被放到任務(wù)源中,瀏覽器內(nèi)部執(zhí)行轉(zhuǎn)移到JavaScript/DOM領(lǐng)域,并且確保這些 tasks按序執(zhí)行。在tasks執(zhí)行期間,瀏覽器可能更新渲染。來(lái)自鼠標(biāo)點(diǎn)擊的事件回調(diào)需要安排一個(gè)task,解析HTML和setTimeout同樣需要。
setTimeout延遲給定的時(shí)間,然后為它的回調(diào)安排一個(gè)新的task。這就是為什么 setTimeout在 script end 之后打印:script end 在第一個(gè)task 內(nèi),setTimeout 在另一個(gè) task 內(nèi)。好了,我們快講完了,剩下一點(diǎn)我需要你們堅(jiān)持下……
Mircotasks隊(duì)列通常用于存放一些任務(wù),這些任務(wù)應(yīng)該在正在執(zhí)行的腳本之后立即執(zhí)行,比如對(duì)一批動(dòng)作作出反應(yīng),或者操作異步執(zhí)行避免創(chuàng)建整個(gè)新任務(wù)造成的性能浪費(fèi)。 只要沒有其他JavaScript代碼在執(zhí)行中,并且在每個(gè)task隊(duì)列的任務(wù)結(jié)束時(shí),microtask隊(duì)列就會(huì)被處理。在處理 microtasks 隊(duì)列期間,新添加到 microtasks 隊(duì)列的任務(wù)也會(huì)被執(zhí)行。 microtasks 包括 MutationObserver callbacks。例如上面的例子中的 promise的callback。
一個(gè)settled狀態(tài)的promise(直接調(diào)用resolve或者reject)或者已經(jīng)變成settled狀態(tài)(異步請(qǐng)求被settled)的promise,會(huì)立刻將它的callback(then)放到microtask隊(duì)列里面。這就能保證promise的回調(diào)是異步的,即便promise已經(jīng)變?yōu)閟ettled狀態(tài)。因此一個(gè)已settled的promise調(diào)用.then(yey,nay)時(shí)將立即把一個(gè)microtask任務(wù)加入microtasks任務(wù)隊(duì)列。這就是為什么 promise1 和 promise2 在 script end 之后打印,因?yàn)檎谶\(yùn)行的代碼必須在處理 microtasks 之前完成。promise1 和 promise2 在 setTimeout 之前打印,因?yàn)?microtasks 總是在下一個(gè) task 之前執(zhí)行。
好,一步一步的運(yùn)行:
console.log("script start"); setTimeout(function() { console.log("setTimeout"); }, 0); Promise.resolve().then(function() { console.log("promise1"); }).then(function() { console.log("promise2"); });
沒錯(cuò),就是上面這個(gè),我做了一個(gè) step-by-step 動(dòng)畫圖解。你周六是怎么過的?和朋友們一起出去玩?我沒有出去。嗯,如果搞不明白我的令人驚嘆的UI設(shè)計(jì)界面,點(diǎn)擊上面的箭頭試試。
瀏覽器實(shí)現(xiàn)差異
一些瀏覽器的打印結(jié)果:
script start script end setTimeout promise1 promise2
在 setTimeout 之后運(yùn)行 promise 的回調(diào),就好像將 promise 的回調(diào)當(dāng)作一個(gè)新的 task 而不是 microtask。
這多少情有可原,因?yàn)?promise 來(lái)自 ECMAScript 規(guī)范而不是 HTML 規(guī)范。ECAMScript 有一個(gè)概念 job,和 microtask 相似,但是兩者的關(guān)系在郵件列表討論中沒有明確。不過,一般共識(shí)是 promise 應(yīng)該是 microtask 隊(duì)列的一部分,并且有充足的理由。
將 promise當(dāng)作task(macrotask)會(huì)帶來(lái)一些性能問題,因?yàn)榛卣{(diào)沒有必要因?yàn)閠ask相關(guān)的事(比如渲染)而延遲執(zhí)行。與其它 task 來(lái)源交互時(shí)它也產(chǎn)生不確定性,也會(huì)打斷與其它 API 的交互,不過后面再細(xì)說。
我提交了一條 Edge 反饋,它錯(cuò)誤地將 promises 當(dāng)作 task。WebKit nightly 做對(duì)了,所以我認(rèn)為 Safari 最終會(huì)修復(fù),而 Firefox 43 似乎已經(jīng)修復(fù)。
有趣的是 Safari 和 Firefox 發(fā)生了退化,而之前的版本是對(duì)的。我在想這是否只是巧合。
How to tell if something uses tasks or microtasks動(dòng)手試一試是一種辦法,查看相對(duì)于promise和setTimeout如何打印,盡管這取決于實(shí)現(xiàn)是否正確。
一種方法是查看規(guī)范:
將一個(gè) task 加入隊(duì)列: step 14 of setTimeout
將 microtask 加入隊(duì)列:step 5 of queuing a mutation record
如上所述,ECMAScript 將 microtask 稱為 job:
調(diào)用 EnqueueJob 將一個(gè) microtask 加入隊(duì)列:step 8.a of PerformPromiseThen
現(xiàn)在,讓我們看一個(gè)更復(fù)雜的例子。一個(gè)有心的學(xué)徒 :“但是他們還沒有準(zhǔn)備好”。別管他,你已經(jīng)準(zhǔn)備好了,讓我們開始……
Level 1 bossfight在發(fā)出這篇文章之前,我犯過一個(gè)錯(cuò)誤。下面是一段html代碼:
給出下面的JS代碼,如果click div.inner將會(huì)打印出什么呢?
// 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);
繼續(xù),在查看答案之前先試一試。 線索:logs可能會(huì)發(fā)生多次。
Test it點(diǎn)擊inner區(qū)域觸發(fā)click事件:
click div.inner :
click promise mutate click promise mutate timeout timeout
click div.outer :
click promise mutate timeout
和你猜想的有不同嗎?如果是,你得到的結(jié)果可能也是正確的。不幸的是,瀏覽器實(shí)現(xiàn)并不統(tǒng)一,下面是各個(gè)瀏覽器下測(cè)試結(jié)果:
Who"s right?觸發(fā) click 事件是一個(gè) task,Mutation observer 和 promise 的回調(diào) 加入microtask列隊(duì),setTimeout 回調(diào)加入task列隊(duì)。因此運(yùn)行過程如下:
點(diǎn)擊內(nèi)部區(qū)域觸發(fā)內(nèi)部區(qū)域點(diǎn)擊事件 -> 冒泡到外部區(qū)域 -> 觸發(fā)外部區(qū)域點(diǎn)擊事件 這里要注意一點(diǎn): setTimeout 執(zhí)行時(shí)機(jī)在冒泡之后,因?yàn)橐彩窃趍icrotask之后,準(zhǔn)確的說是在最后的時(shí)機(jī)執(zhí)行了。
堆棧為空之后將會(huì)執(zhí)行microtasks里面的任務(wù)。
由于冒泡, click函數(shù)再一次執(zhí)行。
最后將執(zhí)行setTimeout。
所以 Chrome 是對(duì)的。對(duì)我來(lái)說新發(fā)現(xiàn)是,microtasks 在回調(diào)之后運(yùn)行(只要沒有其它的 Javascript 在運(yùn)行),我原以為它只能在一個(gè)task 的末尾執(zhí)行。這個(gè)規(guī)則來(lái)自 HTML 規(guī)范,調(diào)用一個(gè)回調(diào):
If the stack of script settings objects is now empty, perform a microtask checkpoint
— HTML: Cleaning up after a callback step 3
一個(gè) microtask checkpoint 逐個(gè)檢查 microtask隊(duì)列,除非我們已經(jīng)在處理一個(gè) microtask 隊(duì)列。類似地,ECMAScript 規(guī)范這么說 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"。
對(duì)于 mutation callbacks,F(xiàn)irefox 和 Safari 都正確地在內(nèi)部區(qū)域和外部區(qū)域單擊事件之間執(zhí)行完畢,清空了microtask 隊(duì)列,但是 promises 列隊(duì)的處理看起來(lái)和chrome不一樣。這多少情有可原,因?yàn)?jobs 和 microtasks 的關(guān)系不清楚,但是我仍然期望在事件回調(diào)之間處理。Firefox ticket. Safari ticket.
對(duì)于 Edge,我們已經(jīng)看到它錯(cuò)誤的將 promises 當(dāng)作 task,它也沒有在單擊回調(diào)之間清空 microtask 隊(duì)列,而是在所有單擊回調(diào)執(zhí)行完之后清空,于是總共只有一個(gè) mutate 在兩個(gè) click 之后打印。 Bug ticket.
Level 1 boss"s angry older brother仍然使用上面的例子,假如我們運(yùn)行下面代碼會(huì)怎么樣:
inner.click();
跟之前一樣,它會(huì)觸發(fā) click 事件,不過是通過代碼而不是實(shí)際的交互動(dòng)作。
Try it下面是各個(gè)瀏覽器的運(yùn)行情況:
我發(fā)誓我一直在Chrome 中得到不同的結(jié)果,我已經(jīng)更新了這個(gè)表許許多次了。我覺得我是錯(cuò)誤地測(cè)試了Canary。假如你在 Chrome 中得到了不同的結(jié)果,請(qǐng)?jiān)谠u(píng)論中告訴我是哪個(gè)版本。
Why is it different?這里介紹了它是怎樣發(fā)生的:
將Run srcipt加入Tasks隊(duì)列,將inner.click加入執(zhí)行堆棧:
執(zhí)行click函數(shù):
按順序執(zhí)行,分別將setTimeout加入Tasks隊(duì)列,將Promise MultationObserver加入microtasks隊(duì)列:
click函數(shù)執(zhí)行完畢之后,我們沒有去處理microtasks隊(duì)列的任務(wù),因?yàn)榇藭r(shí)堆棧不為空:
我們不能將 MultationObserver加入microtasks隊(duì)列,因?yàn)橛幸粋€(gè)等待處理的 MultationObserver:
現(xiàn)在堆棧為空了,我們可以處理microtasks隊(duì)列的任務(wù)了:
最終結(jié)果:
通過對(duì)比事件觸發(fā),我們要注意兩個(gè)地方:JS stack是否是空的決定了microtasks隊(duì)列里任務(wù)的執(zhí)行;microtasks隊(duì)列里不能同時(shí)有多個(gè)MultationObserver。
正確的順序是:click, click, promise, mutate, promise, timeout, timeout,似乎 Chrome 是對(duì)的。
在每個(gè)listerner callback被調(diào)用之后:
If the stack of script settings objects is now empty,perform a microtask checkpoint. — HTML: 回調(diào)之后的清理第三步
之前,這意味著 microtasks 在事件回調(diào)之間運(yùn)行,但是現(xiàn)在.click()讓事件同步觸發(fā),因此調(diào)用.click()的腳本仍處于回調(diào)之間的堆棧中。上面的規(guī)則確保了 microtasks 不會(huì)中斷正在執(zhí)行的JS代碼。這意味著 microtasks 隊(duì)列不會(huì)在事件回調(diào)之間處理,而是在它們之后處理。
Does any of this matter?重要,它會(huì)在偏角處咬你(疼)。我就遇到了這個(gè)問題,我在嘗試為IndexedDB創(chuàng)建一個(gè)使用promises而不是奇怪的IDBRequest對(duì)象的簡(jiǎn)單包裝庫(kù)時(shí)遇到了此問題。它讓 IDB 用起來(lái)很有趣。
當(dāng) IDB 觸發(fā)成功事件時(shí),相關(guān)的 transaction 對(duì)象在事件之后轉(zhuǎn)為非激活狀態(tài)(第四步)。如果我創(chuàng)建的 promise 在這個(gè)事件發(fā)生時(shí)被resolved,回調(diào)應(yīng)當(dāng)在第四步之前執(zhí)行,這時(shí)這個(gè)對(duì)象仍然是激活狀態(tài)。但是在 Chrome 之外的瀏覽器中不是這樣,導(dǎo)致這個(gè)庫(kù)有些無(wú)用。
實(shí)際上你可以在 Firefox 中解決這個(gè)問題,因?yàn)?promise polyfills 如 es6-promise 使用 mutation observers 執(zhí)行回調(diào),它正確地使用了 microtasks。而它在 Safari 下似乎存在競(jìng)態(tài)條件,不過這可能是因?yàn)樗麄冊(cè)愀獾?IDB 實(shí)現(xiàn)。不幸的是 IE/Edge 不一致,因?yàn)?mutation 事件不在回調(diào)之后處理。
希望不久我們能看到一些互通性。
You made it!總結(jié):
tasks 按序執(zhí)行,瀏覽器會(huì)在 tasks 之間執(zhí)行渲染。
microtasks 按序執(zhí)行,在下面情況時(shí)執(zhí)行:
在每個(gè)回調(diào)之后,只要沒有其它代碼正在運(yùn)行。
在每個(gè) task 的末尾。
希望你現(xiàn)在明白了事件循環(huán),或者至少得到一個(gè)借口出去走一走,躺一躺。
呃,還有人在嗎?Hello?Hello?
感謝 Anne van Kesteren, Domenic Denicola, Brian Kardell 和 Matt Gaunt 校對(duì)和修正。是的,Matt 最后還是看了此文,我不必把他整成發(fā)條橙了。
三 后記 總結(jié)1.microtask隊(duì)列就會(huì)被處理的時(shí)機(jī)
(1)只要沒有其他JavaScript代碼在執(zhí)行中, (2)并且在每個(gè)task隊(duì)列的任務(wù)結(jié)束時(shí), microtask隊(duì)列就會(huì)被處理。
也就是說可以在執(zhí)行一個(gè)task之后連續(xù)執(zhí)行多個(gè)microtask。
2. promise相關(guān)
(1)promise一旦創(chuàng)建就會(huì)馬上執(zhí)行 (2)當(dāng)狀態(tài)變?yōu)閟ettled的時(shí)候,callback才會(huì)被加入microtask 隊(duì)列
所以要注意promise創(chuàng)建和callback被執(zhí)行的時(shí)機(jī)。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/94877.html
摘要:事件循環(huán)持續(xù)運(yùn)行,執(zhí)行列隊(duì)。因此一個(gè)已解決的調(diào)用將立即把一個(gè)加入隊(duì)列。如上所述,將稱為。這意味著隊(duì)列在事件回調(diào)之間不處理,而是在它們之后處理。當(dāng)觸發(fā)成功事件時(shí),相關(guān)的對(duì)象在事件之后轉(zhuǎn)為非激活狀態(tài)第四步。 原文:Tasks, microtasks, queues and schedules git地址:Tasks(任務(wù)), microtasks(微任務(wù)), queues(隊(duì)列) an...
摘要:眾所周知和都屬于上述異步任務(wù)的一種那到底為什么和會(huì)有順序之分這就是我想分析總結(jié)的問題所在了和的作用是為了讓瀏覽器能夠從內(nèi)部獲取的內(nèi)容并確保執(zhí)行棧能夠順序進(jìn)行。只要執(zhí)行棧沒有其他在執(zhí)行,在每個(gè)結(jié)束時(shí),隊(duì)列就會(huì)在回調(diào)后處理。 前言 我是在做前端面試題中看到了setTimeout和Promise的比較,然后第一次看到了microtask和macrotask的概念,在閱讀了一些文章之后發(fā)現(xiàn)沒有...
摘要:本文圍繞瀏覽器的事件循環(huán),而有自己的另一套事件循環(huán)機(jī)制,不在本文討論范圍?,F(xiàn)在我們知道了瀏覽器運(yùn)行時(shí)有一個(gè)叫事件循環(huán)的機(jī)制。將事件循環(huán)的當(dāng)前運(yùn)行任務(wù)設(shè)置為。對(duì)于相應(yīng)事件循環(huán)的每個(gè)環(huán)境設(shè)置對(duì)象通知它們哪些為。 本文圍繞瀏覽器的事件循環(huán),而node.js有自己的另一套事件循環(huán)機(jī)制,不在本文討論范圍。網(wǎng)上的許多相關(guān)技術(shù)文章提到了process.nextTick和setImmediate兩個(gè)n...
摘要:簡(jiǎn)介我把在瀏覽器中運(yùn)行主要分為以下幾種類型的任務(wù)同步任務(wù)同步任務(wù)是指按照正常順序執(zhí)行的代碼,比如函數(shù)調(diào)用,數(shù)值運(yùn)算等等,只要是執(zhí)行后立即能夠得到結(jié)果的就是同步任務(wù)。取出微任務(wù)隊(duì)列中的任務(wù)執(zhí)行,直到隊(duì)列被完全清空重復(fù)和,直到宏任務(wù)隊(duì)列被清空。 簡(jiǎn)介 ? 我把JavaScript在瀏覽器中運(yùn)行主要分為以下幾種類型的任務(wù): 同步任務(wù)(MainTask) :同步任務(wù)是指JavaScr...
摘要:但是導(dǎo)致了很明顯的性能問題。上述兩個(gè)例子其實(shí)是在這個(gè)中找到的,第一個(gè)使用的版本是,這個(gè)版本的實(shí)現(xiàn)是采用了,而后因?yàn)榈睦锏挠校谑怯扔晗牧藢?shí)現(xiàn),換成了,也就是后一個(gè)所使用的。后來(lái)尤雨溪了解到是將回調(diào)放入的隊(duì)列。 結(jié)論 對(duì)于event loop 可以抽象成一段簡(jiǎn)單的代碼表示 for (macroTask of macroTaskQueue) { // 1. Handle cur...
閱讀 1273·2021-11-17 09:33
閱讀 3696·2021-09-28 09:42
閱讀 3468·2021-09-13 10:35
閱讀 2682·2021-09-06 15:00
閱讀 2515·2021-08-27 13:12
閱讀 3670·2021-07-26 23:38
閱讀 2004·2019-08-30 15:55
閱讀 604·2019-08-30 15:53