event loop
網上看到的一篇文章,關於介紹task和Tasks, microtasks, queues and schedules,嘗試簡單翻譯一下寫進來吧!
原文地址:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
當我跟我同事Matt Gaunt講,我要寫一篇關於microtask和瀏覽器事件輪詢的文章的時候,他說:“你盡管寫,反正我不看。”好吧,不看就算了,但我還是要寫,總有人會看的。
事實上, Philip Roberts已經對這方面的知識做了一個很完整的介紹,盡管沒有包含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 and desktop Safari 8.0.8中,setTimeout可能會在promise1和promise2之前打印-看起來就像是在競爭。看起來很奇怪,一般都是正確打印的。
這是為啥呢?
想要理解這個,必須先了解事件輪詢中的tasks與microtasks。這里面包含不少知識,第一次接觸這個可能會讓你腦闊疼,請深呼吸:
每一個‘線程’都有它獨立的事件輪詢,所以每個頁面都可以各自工作,執行它們自己的代碼。所有一個來源的窗口都共享同一個事件輪詢,彼此之間同步交流信息。事件輪詢不斷的運轉,執行所有的任務隊列。一個事件輪詢中的任務可能來源於多個地方,需要保證所有任務按正確的順序執行並不簡單,但是瀏覽器會幫忙選擇如何執行這些任務。這樣一來,瀏覽器可以對一些影響性能的操作(如用戶輸入)做特殊處理。跟上!
Tasks已經被提前排好序,保證了瀏覽器可以持續從內部取出它們並弄到JS/DOM中執行。在兩個任務的執行空隙,瀏覽器可能會重新渲染視圖。在解析HTML頁面的時候,鼠標點擊事件與對應回調函數會產生一個新的task,同時會產生事件序列的還有上面的例子:setTimeout。
setTimeout會延遲指定的時間,然后將回調函數加入任務序列中。這就是為什么setTimeout會在script end后面打印。script end的打印屬於第一個任務序列的一部分,而setTimeout則在下一個任務序列中被打印。OK,這里基本上沒問題了,希望下一個環節你還能堅持住……
Microtasks通常在JS當前主任務執行完后直接執行,比如說對一些特殊事件作出響應,或者在不影響主線程情況下異步執行某些事件。一旦沒有其他JS代碼在執行中,microtask隊列會立即執行,執行過程中如果有microtask插入,也同時會被執行。microtas包括mutation observer回調,與上面例子中的promise回調。
一旦一個promise被決議,在決議后就會形成一個microtask來響應回調函數。這個可以保證promise的即使被決議,回調函數也會被異步執行。因此,調用then(rel,rej)方法后會立即生成一個microtask隊列。這也就是為什么promise1和promise2在script end后面打印,microtask必須在當前JS代碼運轉完后才會被操作。promise1和promise2在setTimeout之前打印,也就是microtask永遠在下一個task之前執行。
這樣上面的例子就很清晰了:
//執行JS主代碼
console.log('script start'); //等待下一輪task
setTimeout(function() { console.log('setTimeout'); }, 0); //then方法產生microtask
Promise.resolve().then(function() { console.log('promise1'); //又插入一個microtask 立即執行
//執行完后進行下一輪task
}).then(function() { console.log('promise2'); }); //JS主代碼 第一輪task執行完會執行microtask
console.log('script end');
如同注釋所說的那樣,一步一步得到了最后的結果。
為什么不同瀏覽器會出現差異?
有些瀏覽器會打印script start,start end,setTimeout,promise1,promise2。promise的回調函數在setTimeout之后執行,看起來似乎將promise當成下一輪task而不是microtask。
某種程度上可以理解這件事,promise來自於ECMA標准而不是HTML。ECMA標准中有'jobs'的概念,跟microtasks很相似,然而,僅僅通過一些類似郵件的討論,這兩者的區別並不是那么清晰。但是一般來說,都公認promise應該是microtask的一部分,而且確實比較好。
promise一般用來解決性能問題,有些回調函數可能會因為渲染之類的事件導致延遲執行。(后面的沒太看懂)
如何判定這是一個task還是一個microtask
測試是一種方式。看看setTimeout和promise的打印順序,當然保證結果是正確。
比較穩妥的方式是看說明文檔。(舉的例子會跳轉到一個新頁面)
稍微提一下,在ECMA標准中,microtask被叫做‘jobs’。在step 8.a of PerformPromiseThen中,排入隊列被稱為生成一個microtask序列。
現在來看一個更加復雜的案例:
Lv1 BOSS
寫這部分之前,我先給點易錯的案例。
看看下面這一段html:
<div class="outer">
<div class="inner"></div>
</div>
JS代碼如下:
var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); new MutationObserver(function() { console.log('mutate'); }).observe(outer, { attributes: true }); function onClick() { console.log('click'); setTimeout(function() { console.log('timeout'); }, 0); Promise.resolve().then(function() { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } inner.addEventListener('click', onClick); outer.addEventListener('click', onClick);
如果點擊div.inner,會打印出什么呢?
在查看答案之前自己分析一下。(提示:有些東西會不止打印一次)
答案不一樣?或許你是對的,因為不同瀏覽器打印的不一樣。
Chrome:click,promise,mutate,click,promise,mutate,timeoue,timeoue
Firefox:click,mutate,click,mutate,timeoue,promise,promise,timeoue
Safari:click,mutate,click,mutate,promise,promise,timeout,timeout
IE:click,click,mutate,timeout,promise,timeout,promise
哪一個是對的?
觸發的click事件是一個task。Mutation observer和promise的回調函數是microtask。setTimeout是另外一個task。所以順序這樣是這樣的;
new MutationObserver(function() { //緊跟在promise后面的microtask
console.log('mutate'); }).observe(outer, { attributes: true }); function onClick() { //click第一個task
console.log('click'); //第二個task
setTimeout(function() { console.log('timeout'); }, 0); //promise產生一個microtask
Promise.resolve().then(function() { console.log('promise'); }); //這句代碼也會產生一個microtask
outer.setAttribute('data-random', Math.random()); }
過程大概是這樣的:點擊div.inner,click(第一個task)->timeout(第二個task)->promise(microtask)->mutate(microtask)。
按照之前所描述的順序:task->microtask->task,可以得到click,promise,mutate,timeout。但是由於冒泡的關系,外層div也會觸發一遍上面的流程,所以最終結果是click,promise,mutate,click,promise,mutae,timeout,timeout。
因此,Chrome是正確的。有一個地方對我來說很新鮮,microtask的回調會在沒有其余運行中JS代碼后執行,我理解為task的尾部。下面是HTML文檔中對回調的說明:
如果棧中JS環境對象為空,會執行microtask隊列的檢查。
—HTML:Cleaning up after a callback
microtask的檢查包含:遍歷microtask隊列直到全部被執行。
ECMA標准把這個稱為jobs:
只有當前環境沒有任何東西在執行並且執行環境棧為空,job才能開始被執行。
—ECMAScript:Jobs and Job Queues
在HTML環境中,'能'變成了'必須'。
為什么瀏覽器會出錯?
Firefox和Safari可以正確的區別microtask與click事件,比如mutation的回調函數,但是promise的處理不太一樣。這個順序會出現問題也是情有可原的,因為關於job和microtask之間區別非常模糊,我認為這兩個在事件回調之間執行比較合理。 Firefox ticket. Safari ticket(這里是兩個相關bug討論鏈接)
至於Edge,它對promise的處理錯的一塌糊塗,同時也未在兩個監聽事件之間執行microtask隊列,等監聽事件都完事了才調用microtask,並且兩個click事件只打印了一次mutate。Bug ticket
Lv1 BOSS憤怒的哥哥
代碼跟上面的一樣,但是執行的代碼變成了:
inner.click();
這個也會觸發同樣的事件,但是方式不是通過點擊,而是直接用JS代碼執行。
答案如下:
Chrome:click,click,promise,mutate,promise,timeout,timeout
Firefox:click,click,mutate,timeout,promise,promise,timeout
Safari:click,click,muate,promise,promise,timeout,timeout
IE:click,click,mutate,timeout,promise,timeout,promise
Chrome每次都會出現不同的結果,我專門弄了一個表來記錄我測試出來的錯誤。如果你在Chrome中得到不一樣的結果,在評論中告訴我版本號。
為啥不一樣?
來梳理一下流程。
首先這里有一個不一樣的地方,即之前提到的:這里是執行JS代碼觸發函數,不是事件觸發。所以這里的順序是task(執行JS代碼)->task(onClick函數)->打印click->timeout(第二個task)->promise(microtask)->mutate(microtask)->打印click(冒泡)->timeout(第三個task)->promise(microtask),mutate只會觸發一次(不太懂原理),主要區別在於在冒泡的時候,JS代碼仍在執行,所以說microtask不會執行,必須等到第二個click打印才會觸發。最后正確的結果是click,click,promise,mutate,promise,timeout,timeout,看起來Chrome又對了。
microtask在兩個事件監聽觸發后被調用。
之前,microtask在監聽回調之間執行,但是通過JS代碼的函數調用,導致事件同步執行了,第一個回調結束后,JS主線程依舊在棧中。上述規則保證了microtask不打斷JS主線程執行。這意味着這種情況下,microtask不能在監聽回調之間執行,而需要在之后。
這沒問題嗎?
可能你會在一些地方還存在疑惑。我曾經在試圖創建a simple wrapper library for IndexedDB that uses promises遇到過這個問題,它比IDBRequest對象還要奇怪。這同時也讓IDB變得好玩起來。almost makes IDB fun to use.
當IDB成功執行一個事件,相關的事務對象變得沒那么活躍了(transaction object becomes inactive after dispatching沒看懂)。如果在事件執行期間創建了一個promise,該回調會在step4(?)之前執行,此時相關事件仍然在執行,這個現象只會在Chrome中出現,對渲染庫有一點沒用。
在Firefox中你可以變通解決這個問題,因為promise的polyfill是用mutation observers實現的,正確的實現了microtask。Safari對這兩個microtask一直處於糾結狀態。不幸的是,IE/Edge中也會有問題,mutation不會在回調后執行。
希望能從這些問題中找到一些共同點。
額
總結一下:
· task按順序執行,瀏覽器可能在周期間隙里渲染視圖
· microtask也是按順序執行,遵循下列規則:
1.JS主線程沒有程序執行
2.主程序的尾部
希望現在你能明白關於事件輪詢的相關概念,至少會分析執行順序。
有人在看么。。。(作者原話)
鳴謝A,B,C……
