摘要:事件循環持續運行,執行列隊。因此一個已解決的調用將立即把一個加入隊列。如上所述,將稱為。這意味著隊列在事件回調之間不處理,而是在它們之后處理。當觸發成功事件時,相關的對象在事件之后轉為非激活狀態第四步。
原文:Tasks, microtasks, queues and schedules
git地址:Tasks(任務), microtasks(微任務), queues(隊列) and schedules(回調隊列)
如果你更喜歡視頻,Philip Roberts 在 JSConf 上就事件循環有一個很棒的演講——沒有講 microtasks,不過很好的介紹了其它概念。好,繼續!
試一下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");
結果是: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 是對的。
為什么是這樣的要想弄明白為什么,你需要知道事件循環如何處理 tasks 和 microtasks。第一次接觸需要花些功夫才能弄明白。深呼吸……
每個線程都有自己的事件循環,所以每個 web worker 有自己的事件循環(event loop),所以它能獨立地運行。而所有同源的 window 共享一個事件循環,因為它們能同步的通訊。事件循環持續運行,執行 tasks 列隊。一個事件循環有多個 task 來源,并且保證在 task 來源內的執行順序(IndexedDB 等規范定義了自己的 task 來源),在每次循環中瀏覽器要選擇從哪個來源中選取 task,這使得瀏覽器能優先執行敏感 task,例如用戶輸入。Ok ok, 留下來陪我坐會兒……
Tasks 被列入隊列,于是瀏覽器能從它的內部轉移到 Javascript/DOM 領地,并且確使這些 tasks 按序執行。在 tasks 之間,瀏覽器可以更新渲染。來自鼠標點擊的事件回調需要安排一個 task,解析 HTML 和 setTimeout 同樣需要。
setTimeout 延遲給定的時間,然后為它的回調安排一個新的 task。這就是為什么 setTimeout 在 script end 之后打印,script end 在第一個 task 內,setTimeout 在另一個 task 內。好了,我們快講完了,剩下一點我需要你們堅持下……
Mircotasks 通常用于安排一些事,它們應該在正在執行的代碼之后立即發生,例如響應操作,或者讓操作異步執行,以免付出一個全新 task 的代價。mircotask 隊列在回調之后處理,只要沒有其它執行當中的(mid-execution)代碼;或者在每個 task 的末尾處理。在處理 microtasks 隊列期間,新添加的 microtasks 添加到隊列的末尾并且也被執行。 microtasks 包括 mutation observer 回調。上面的例子中的 promise 的回調也是。
promise 一旦解決(settled),或者已解決,它便為它的回調安排一個 microtask。這確使 promise 回調是異步的,即便 promise 已經解決。因此一個已解決的 promise 調用 .then(yey, nay) 將立即把一個 microtask 加入隊列。這就是為什么 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"); });
(注:原文表示運行結果為:"script start", "script end", "promise1", "promise2", "setTimeout")
為什么有些瀏覽器表現不一致一些瀏覽器的打印結果:script start, script end, setTimeout, promise1, promise2。在 setTimeout 之后運行 promise 的回調,就好像將 promise 的回調當作一個新的 task 而不是 microtask。
這多少情有可原,因為 promise 來自 ECMAScript 規范而不是 HTML 規范。ECAMScript 有一個概念 job,和 microtask 相似,但是兩者的關系在郵件列表討論中沒有明確。不過,一般共識是 promise 應該是 microtask 隊列的一部分,并且有充足的理由。
將 promise 當作 task 會導致性能問題,因為回調可能不必要地被與 task 相關的事(比如渲染)延遲。與其它 task 來源交互時它也導致不確定性,也會打斷與其它 API 的交互,不過后面再細說。
我提交了一條 Edge 反饋,它錯誤地將 promises 當作 task。WebKit nightly 做對了,所以我認為 Safari 最終會修復,而 Firefox 43 似乎已經修復。
有趣的是 Safari 和 Firefox 發生了退化,而之前的版本是對的。我在想這是否只是巧合。
怎么判斷是task還是mictask測試是一種辦法,查看相對于 promise 和 setTimeout 如何打印,盡管這取決于實現是否正確。
一種方法是查看規范。例如,setTimeout 的第十四步將一個 task 加入隊列,mutation record 的第五步將 microtask 加入隊列。
如上所述,ECMAScript 將 microtask 稱為 job。PerformPromiseThen 的第八步 調用 EnqueueJob 將一個 microtask 加入隊列。
現在,讓我們看一個更復雜的例子。(一個有心的學徒 :“但是他們還沒有準備好”。別管他,你已經準備好了,讓我們開始……)
level 1在寫這篇文章之前我一直 會在這里出錯。下面是 html 代碼片段:
有如下的 Javascript 代碼,假如我點擊 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);
在給出答案前先實際運行一下吧
測試點擊里面的矩形觸發一個 click 事件:
你的猜測是否不同?若是,你也可能是對的。但不幸的是各瀏覽器不一致:
click | click | click | click |
promise | mutate | click | mutate |
mutate | click | mutate | click |
click | mutate | timeout | mutate |
promise | timeout | promise | promise |
mutate | promise | timeout | promise |
timeout | promise | promise | timeout |
timeout | timeout | timeout |
一個 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
觸發 click 事件是一個 task,Mutation observer 和 promise 回調作為 microtask 加入列隊,setTimeout 回調作為 task 加入列隊。因此運行過程如下:
// 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 });
所以 Chrome 是對的。對我來說新發現是,microtasks 在回調之后運行(只要沒有其它的 Javascript 在運行,我原以為它只能在 task 的末尾運行。這個規則來自 HTML 規范,調用一個回調:
If the stack of script settings objects is now empty,perform a microtask checkpoint。
— HTML: 回調之后的清理第三步
一個 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 回調,Firefox 和 Safari 正確地在單擊回調之間清空 microtask 隊列,但是 promises 列隊似乎不一樣。這多少情有可原,因為 jobs 和 microtasks 的關系不清楚,但是我仍然期望在事件回調之間處理。Firefox bug。 Safari bug。
對于 Edge,我們已經看到它錯誤的將 promises 當作 task,它也沒有在單擊回調之間清空 microtask 隊列,而是在所有單擊回調執行完之后清空,于是總共只有一個 mutate 在兩個 click 之后打印。 Edge bug。
level 1 憤怒的老大哥我們用同樣的例子運行:
inner.click();
跟之前一樣,它會觸發 click 事件,不過是通過代碼而不是實際的交互動作。
試一下click | click | click | click |
click | click | click | click |
promise | mutate | mutate | mutate |
mutate | timeout | timeout | promise |
promise | promise | promise | promise |
timeout | promise | timeout | timeout |
timeout | timeout | promise | timeout |
我發誓我在 Chrome 中始終得到不同的結果,我更新了這個表許多次才意識到我測試的是 Canary。假如你在 Chrome 中得到了不同的結果,請在評論中告訴我是哪個版本。
為什么不同它應該像下面這樣運行:
// 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); inner.click();
正確的順序是:click, click, promise, mutate, promise, timeout, timeout,似乎 Chrome 是對的。
在每個事件回調調用之后:
If the stack of script settings objects is now empty,perform a microtask checkpoint.
— HTML: 回調之后的清理第三步
之前,這意味著 microtasks 在事件回調之間運行,但是 .click() 讓事件同步觸發,所以調用 .click() 的代碼仍然在事件回調之間的棧內。上面的規則確保了 microtasks 不會中斷執行當中的代碼。這意味著 microtasks 隊列在事件回調之間不處理,而是在它們之后處理。
這重要嗎重要,它會在偏角處咬你(疼)。我就遇到了這個問題,在我嘗試用 promises 而不是用怪異的 IDBRequest 對象為 IndexedDB 創建一個簡單的包裝庫 時。它讓 IDB 用起來很有趣。
當 IDB 觸發成功事件時,相關的 transaction 對象在事件之后轉為非激活狀態(第四步)。如果我創建的 promise 在這個事件發生時被履行(resolved),回調應當在第四步之前執行,這時這個對象仍然是激活狀態。但是在 Chrome 之外的瀏覽器中不是這樣,導致這個庫有些無用。
實際上你可以在 Firefox 中解決這個問題,因為 promise polyfills 如 es6-promise 使用 mutation observers 執行回調,它正確地使用了 microtasks。而它在 Safari 下似乎存在競態條件,不過這可能是因為他們 糟糕的 IDB 實現。不幸的是 IE/Edge 不一致,因為 mutation 事件不在回調之后處理。
希望不久我們能看到一些互通性。
你做到了!總結:
tasks 按序執行,瀏覽器會在 tasks 之間執行渲染。
microtasks 按序執行,在下面情況時執行:
在每個回調之后,只要沒有其它代碼正在運行。
在每個 task 的末尾
希望你現在明白了事件循環,或者至少得到一個借口出去走一走躺一躺。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/98041.html
摘要:事件循環持續運行,直到清空列隊的任務。在執行期間,瀏覽器可能更新渲染。線索可能會發生多次。由于冒泡,函數再一次執行。這意味著隊列不會在事件回調之間處理,而是在它們之后處理。當觸發成功事件時,相關的對象在事件之后轉為非激活狀態第四步。 一 前言 一直想對異步處理做一個研究,在查閱資料時發現了這篇文章,非常深入的解釋了事件循環中重的任務隊列。原文中有代碼執行工具,強烈建議自己執行一下查看結...
摘要:簡介我把在瀏覽器中運行主要分為以下幾種類型的任務同步任務同步任務是指按照正常順序執行的代碼,比如函數調用,數值運算等等,只要是執行后立即能夠得到結果的就是同步任務。取出微任務隊列中的任務執行,直到隊列被完全清空重復和,直到宏任務隊列被清空。 簡介 ? 我把JavaScript在瀏覽器中運行主要分為以下幾種類型的任務: 同步任務(MainTask) :同步任務是指JavaScr...
摘要:本文圍繞瀏覽器的事件循環,而有自己的另一套事件循環機制,不在本文討論范圍。現在我們知道了瀏覽器運行時有一個叫事件循環的機制。將事件循環的當前運行任務設置為。對于相應事件循環的每個環境設置對象通知它們哪些為。 本文圍繞瀏覽器的事件循環,而node.js有自己的另一套事件循環機制,不在本文討論范圍。網上的許多相關技術文章提到了process.nextTick和setImmediate兩個n...
摘要:譯文事件循環機制之宏任務微任務原文標題這是一篇谷歌大神文章,寫得非常精彩。為什么會出現這樣打印順序呢要理解這些你首先需要對事件循環機制處理宏任務和微任務的方式有了解。 譯文:JS事件循環機制(event loop)之宏任務、微任務 原文標題:《Tasks, microtasks, queues and schedules》 這是一篇谷歌大神文章,寫得非常精彩。譯者想借這次翻譯深入學習一...
摘要:的宿主最開始本身就是瀏覽器,處理用戶的交互事件。既然是單線程的,那就意味著任務需要排隊,只有前一個任務執行完畢,下一個任務才能開始,于是就有了任務隊列。事件循環有兩種用于瀏覽上下文的事件循環和用于的事件循環。 最近看到Event Loop這個詞出現的頻率有點高,于是查閱各方資料在此記錄一下。 先不說概念,我們來看段代碼: console.log(script start); setT...
閱讀 3094·2021-08-03 14:05
閱讀 2140·2019-08-29 15:35
閱讀 678·2019-08-29 13:30
閱讀 3169·2019-08-29 13:20
閱讀 2530·2019-08-23 18:15
閱讀 1796·2019-08-23 14:57
閱讀 2213·2019-08-23 13:57
閱讀 1310·2019-08-23 12:10