摘要:規范中定義了瀏覽器何時進行渲染更新,了解它有助于性能優化。結合一些資料,對上邊規范給出一些理解有誤請指正每個線程都有自己的。列為,列為,列為。我們都知道是單線程,渲染計算和腳本運行共用同一線程網絡請求會有其他線程,導致腳本運行會阻塞渲染。
本文轉自blog
轉載請注明出處
異步的思考event loops隱藏得比較深,很多人對它很陌生。但提起異步,相信每個人都知道。異步背后的“靠山”就是event loops。這里的異步準確的說應該叫瀏覽器的event loops或者說是javaScript運行環境的event loops,因為ECMAScript中沒有event loops,event loops是在HTML Standard定義的。
event loops規范中定義了瀏覽器何時進行渲染更新,了解它有助于性能優化。
思考下邊的代碼運行順序:
console.log("start") setTimeout( function () { console.log("setTimeout") }, 0 ) Promise.resolve().then(function() { console.log("promise1"); }).then(function() { console.log("promise2"); }); console.log("end") // start // end // promise1 // promise2 // setTimeout
上面的順序是在chrome運行得出的,有趣的是在safari 9.1.2中測試,promise1 promise2會在setTimeout的后邊,而在safari 10.0.1中得到了和chrome一樣的結果。為何瀏覽器有不同的表現,了解tasks, microtasks隊列就可以解答這個問題。
很多框架和庫都會使用類似下面函數:
function flush() { ... } function useMutationObserver() { var iterations = 0; var observer = new MutationObserver(flush); var node = document.createTextNode(""); observer.observe(node, { characterData: true }); return function () { node.data = iterations = ++iterations % 2; }; }
初次看這個useMutationObserver函數總會很有疑惑,MutationObserver不是用來觀察dom的變化的嗎,這樣憑空造出一個節點來反復修改它的內容,來觸發觀察的回調函數有何意義?
答案就是使用Mutation事件可以異步執行操作(例子中的flush函數),一是可以盡快響應變化,二是可以去除重復的計算。但是setTimeout(flush, 0)同樣也可以執行異步操作,要知道其中的差異和選擇哪種異步方法,就得了解event loop。
定義先看看它們在規范中的定義。
Note:本文的引用部分,就是對規范的翻譯,有的部分會概括或者省略的翻譯,有誤請指正。
event loopevent loop翻譯出來就是事件循環,可以理解為實現異步的一種方式,我們來看看event loop在HTML Standard中的定義章節:
第一句話:
為了協調事件,用戶交互,腳本,渲染,網絡等,用戶代理必須使用本節所述的event loop。
事件,用戶交互,腳本,渲染,網絡這些都是我們所熟悉的東西,他們都是由event loop協調的。觸發一個click事件,進行一次ajax請求,背后都有event loop在運作。
task一個event loop有一個或者多個task隊列。
當用戶代理安排一個任務,必須將該任務增加到相應的event loop的一個tsak隊列中。
每一個task都來源于指定的任務源,比如可以為鼠標、鍵盤事件提供一個task隊列,其他事件又是一個多帶帶的隊列。可以為鼠標、鍵盤事件分配更多的時間,保證交互的流暢。
task也被稱為macrotask,task隊列還是比較好理解的,就是一個先進先出的隊列,由指定的任務源去提供任務。
哪些是task任務源呢?
規范在Generic task sources中有提及:
DOM操作任務源:
此任務源被用來相應dom操作,例如一個元素以非阻塞的方式插入文檔。
用戶交互任務源:
此任務源用于對用戶交互作出反應,例如鍵盤或鼠標輸入。響應用戶操作的事件(例如click)必須使用task隊列。
網絡任務源:
網絡任務源被用來響應網絡活動。
history traversal任務源:
當調用history.back()等類似的api時,將任務插進task隊列。
task任務源非常寬泛,比如ajax的onload,click事件,基本上我們經常綁定的各種事件都是task任務源,還有數據庫操作(IndexedDB ),需要注意的是setTimeout、setInterval、setImmediate也是task任務源。總結來說task任務源:
setTimeout
setInterval
setImmediate
I/O
UI rendering
microtask每一個event loop都有一個microtask隊列,一個microtask會被排進microtask隊列而不是task隊列。
有兩種microtasks:分別是solitary callback microtasks和compound microtasks。規范值只覆蓋solitary callback microtasks。
如果在初期執行時,spin the event loop,microtasks有可能被移動到常規的task隊列,在這種情況下,microtasks任務源會被task任務源所用。通常情況,task任務源和microtasks是不相關的。
microtask 隊列和task 隊列有些相似,都是先進先出的隊列,由指定的任務源去提供任務,不同的是一個
event loop里只有一個microtask 隊列。
HTML Standard沒有具體指明哪些是microtask任務源,通常認為是microtask任務源有:
process.nextTick
promises
Object.observe
MutationObserver
NOTE:
Promise的定義在 ECMAScript規范而不是在HTML規范中,但是ECMAScript規范中有一個jobs的概念和microtasks很相似。在Promises/A+規范的Notes 3.1中提及了promise的then方法可以采用“宏任務(macro-task)”機制或者“微任務(micro-task)”機制來實現。所以開頭提及的promise在不同瀏覽器的差異正源于此,有的瀏覽器將then放入了macro-task隊列,有的放入了micro-task 隊列。在jake的博文Tasks, microtasks, queues and schedules中提及了一個討論vague mailing list discussions,一個普遍的共識是promises屬于microtasks隊列。
知道了event loops大致做什么的,我們再深入了解下event loops。
有兩種event loops,一種在瀏覽器上下文,一種在workers中。
每一個用戶代理必須至少有一個瀏覽器上下文event loop,但是每個單元的相似源瀏覽器上下文至多有一個event loop。
event loop 總是具有至少一個瀏覽器上下文,當一個event loop的瀏覽器上下文全都銷毀的時候,event loop也會銷毀。一個瀏覽器上下文總有一個event loop去協調它的活動。
Worker的event loop相對簡單一些,一個worker對應一個event loop,worker進程模型管理event loop的生命周期。
反復提到的一個詞是browsing contexts(瀏覽器上下文)。
瀏覽器上下文是一個將 Document 對象呈現給用戶的環境。在一個 Web 瀏覽器內,一個標簽頁或窗口常包含一個瀏覽上下文,如一個 iframe 或一個 frameset 內的若干 frame。
結合一些資料,對上邊規范給出一些理解(有誤請指正):
每個線程都有自己的event loop。
瀏覽器可以有多個event loop,browsing contexts和web workers就是相互獨立的。
所有同源的browsing contexts可以共用event loop,這樣它們之間就可以相互通信。
event loop的處理過程(Processing model)在規范的Processing model定義了event loop的循環過程:
一個event loop只要存在,就會不斷執行下邊的步驟:
1.在tasks隊列中選擇最老的一個task,用戶代理可以選擇任何task隊列,如果沒有可選的任務,則跳到下邊的microtasks步驟。
2.將上邊選擇的task設置為正在運行的task。
3.Run: 運行被選擇的task。
4.將event loop的currently running task變為null。
5.從task隊列里移除前邊運行的task。
6.Microtasks: 執行microtasks任務檢查點。(也就是執行microtasks隊列里的任務)
7.更新渲染(Update the rendering)...
8.如果這是一個worker event loop,但是沒有任務在task隊列中,并且WorkerGlobalScope對象的closing標識為true,則銷毀event loop,中止這些步驟,然后進行定義在Web workers章節的run a worker。
9.返回到第一步。
event loop會不斷循環上面的步驟,概括說來:
event loop會不斷循環的去取tasks隊列的中最老的一個任務推入棧中執行,并在當次循環里依次執行并清空microtask隊列里的任務。
執行完microtask隊列里的任務,有可能會渲染更新。(瀏覽器很聰明,在一幀以內的多次dom變動瀏覽器不會立即響應,而是會積攢變動以最高60HZ的頻率更新視圖)
microtasks檢查點(microtask checkpoint)event loop運行的第6步,執行了一個microtask checkpoint,看看規范如何描述microtask checkpoint:
當用戶代理去執行一個microtask checkpoint,如果microtask checkpoint的flag(標識)為false,用戶代理必須運行下面的步驟:
1.將microtask checkpoint的flag設為true。
2.Microtask queue handling: 如果event loop的microtask隊列為空,直接跳到第八步(Done)。
3.在microtask隊列中選擇最老的一個任務。
4.將上一步選擇的任務設為event loop的currently running task。
5.運行選擇的任務。
6.將event loop的currently running task變為null。
7.將前面運行的microtask從microtask隊列中刪除,然后返回到第二步(Microtask queue handling)。
8.Done: 每一個environment settings object它們的 responsible event loop就是當前的event loop,會給environment settings object發一個 rejected promises 的通知。
9.清理IndexedDB的事務。
10.將microtask checkpoint的flag設為flase。
microtask checkpoint所做的就是執行microtask隊列里的任務。什么時候會調用microtask checkpoint呢?
當上下文執行棧為空時,執行一個microtask checkpoint。
在event loop的第六步(Microtasks: Perform a microtask checkpoint)執行checkpoint,也就是在運行task之后,更新渲染之前。
執行棧(JavaScript execution context stack)task和microtask都是推入棧中執行的,要完整了解event loops還需要認識JavaScript execution context stack,它的規范位于https://tc39.github.io/ecma26...。
javaScript是單線程,也就是說只有一個主線程,主線程有一個棧,每一個函數執行的時候,都會生成新的execution context(執行上下文),執行上下文會包含一些當前函數的參數、局部變量之類的信息,它會被推入棧中, running execution context(正在執行的上下文)始終處于棧的頂部。當函數執行完后,它的執行上下文會從棧彈出。
舉個簡單的例子:
function bar() { console.log("bar"); } function foo() { console.log("foo"); bar(); } foo();
執行過程中棧的變化:
規范晦澀難懂,做一個形象的比喻:
主線程類似一個加工廠,它只有一條流水線,待執行的任務就是流水線上的原料,只有前一個加工完,后一個才能進行。event loops就是把原料放上流水線的工人。只要已經放在流水線上的,它們會被依次處理,稱為同步任務。一些待處理的原料,工人會按照它們的種類排序,在適當的時機放上流水線,這些稱為異步任務。
過程圖:
舉個簡單的例子,假設一個script標簽的代碼如下:
Promise.resolve().then(function promise1 () { console.log("promise1"); }) setTimeout(function setTimeout1 (){ console.log("setTimeout1") Promise.resolve().then(function promise2 () { console.log("promise2"); }) }, 0) setTimeout(function setTimeout2 (){ console.log("setTimeout2") }, 0)
運行過程:
script里的代碼被列為一個task,放入task隊列。
循環1:
【task隊列:script ;microtask隊列:】
從task隊列中取出script任務,推入棧中執行。
promise1列為microtask,setTimeout1列為task,setTimeout2列為task。
【task隊列:setTimeout1 setTimeout2;microtask隊列:promise1】
script任務執行完畢,執行microtask checkpoint,取出microtask隊列的promise1執行。
循環2:
【task隊列:setTimeout1 setTimeout2;microtask隊列:】
從task隊列中取出setTimeout1,推入棧中執行,將promise2列為microtask。
【task隊列:setTimeout2;microtask隊列:promise2】
執行microtask checkpoint,取出microtask隊列的promise2執行。
循環3:
【task隊列:setTimeout2;microtask隊列:】
從task隊列中取出setTimeout2,推入棧中執行。
setTimeout2任務執行完畢,執行microtask checkpoint。
【task隊列:;microtask隊列:】
event loop中的Update the rendering(更新渲染)這是event loop中很重要部分,在第7步會進行Update the rendering(更新渲染),規范允許瀏覽器自己選擇是否更新視圖。也就是說可能不是每輪事件循環都去更新視圖,只在有必要的時候才更新視圖。
我們都知道javaScript是單線程,渲染計算和腳本運行共用同一線程(網絡請求會有其他線程),導致腳本運行會阻塞渲染。
https://www.html5rocks.com/zh... 這篇文章較詳細的講解了渲染機制。
渲染的基本流程:
處理 HTML 標記并構建 DOM 樹。
處理 CSS 標記并構建 CSSOM 樹, 將 DOM 與 CSSOM 合并成一個渲染樹。
根據渲染樹來布局,以計算每個節點的幾何信息。
將各個節點繪制到屏幕上。
Note: 可以看到渲染樹的一個重要組成部分是CSSOM樹,繪制會等待css樣式全部加載完成才進行,所以css樣式加載的快慢是首屏呈現快慢的關鍵點。
下面討論一下渲染的時機。規范定義在一次循環中,Update the rendering會在第六步Microtasks: Perform a microtask checkpoint 后運行。
驗證更新渲染(Update the rendering)的時機不同機子測試可能會得到不同的結果,這取決于瀏覽器,cpu、gpu性能以及它們當時的狀態。
例子1我們做一個簡單的測試
this is con
用chrome的Developer tools的Timeline查看各部分運行的時間點。
當我們點擊這個div的時候,下圖截取了部分時間線,黃色部分是腳本運行,紫色部分是更新render樹、計算布局,綠色部分是繪制。
綠色和紫色部分可以認為是Update the rendering。
在這一輪事件循環中,setTimeout1是作為task運行的,可以看到paint確實是在task運行完后才進行的。
例子2現在換成一個microtask任務,看看有什么變化
this is con
和上一個例子很像,不同的是這一輪事件循環的task是click的回調函數,Promise1則是microtask,paint同樣是在他們之后完成。
標準就是那么定義的,答案似乎顯而易見,我們把例子變得稍微復雜一些。
例子3this is con
當點擊后,一共產生3個task,分別是click1、setTimeout1、setTimeout2,所以會分別在3次event loop中進行。
下面截取的是setTimeout1、setTimeout2的部分。
我們修改了兩次textContent,奇怪的是setTimeout1、setTimeout2之間沒有paint,瀏覽器只繪制了textContent=1,難道setTimeout1、setTimeout2在同一次event loop中嗎?
例子4在兩個setTimeout中增加microtask。
this is con
從run microtasks中可以看出來,setTimeout1、setTimeout2應該運行在兩次event loop中,textContent = 0的修改被跳過了。
setTimeout1、setTimeout2的運行間隔很短,在setTimeout1完成之后,setTimeout2馬上就開始執行了,我們知道瀏覽器會盡量保持每秒60幀的刷新頻率(大約16.7ms每幀),是不是只有兩次event loop間隔大于16.7ms才會進行繪制呢?
例子5將時間間隔加大一些。
this is con
兩塊黃色的區域就是 setTimeout,在1224ms處綠色部分,瀏覽器對con.textContent = 0的變動進行了繪制。在1234ms處綠色部分,繪制了con.textContent = 1。
可否認為相鄰的兩次event loop的間隔很短,瀏覽器就不會去更新渲染了呢?繼續我們的實驗
例子6我們在同一時間執行多個setTimeout來模擬執行間隔很短的task。
this is con
圖中一共繪制了兩幀,第一幀4.4ms,第二幀9.3ms,都遠遠高于每秒60HZ(16.7ms)的頻率,第一幀繪制的是con.textContent = 4,第二幀繪制的是 con.textContent = 6。所以兩次event loop的間隔很短同樣會進行繪制。
例子7有說法是一輪event loop執行的microtask有數量限制(可能是1000),多余的microtask會放到下一輪執行。下面例子將microtask的數量增加到25000。
this is con
總體的timeline:
可以看到一大塊黃色區域,上半部分有一根綠線就是點擊后的第一次繪制,腳本的運行耗費大量的時間,并且阻塞了渲染。
看看setTimeout2的運行情況。
可以看到setTimeout2這輪event loop沒有run microtasks,microtasks在setTimeout1被全部執行完了。
25000個microtasks不能說明event loop對microtasks數量沒有限制,有可能這個限制數很高,遠超25000,但日常使用基本不會使用那么多了。
對microtasks增加數量限制,一個很大的作用是防止腳本運行時間過長,阻塞渲染。
例子8使用requestAnimationFrame。
this is con
總體的Timeline:
點擊后繪制了3幀,把每次變動都繪制了。
看看單個 requestAnimationFrame的Timeline:
和setTimeout很相似,可以看出requestAnimationFrame也是一個task,在它完成之后會運行run microtasks。
例子9驗證postMessage是否是task
setTimeout(function setTimeout1(){ console.log("setTimeout1") }, 0) var channel = new MessageChannel(); channel.port1.onmessage = function onmessage1 (){ console.log("postMessage") Promise.resolve().then(function promise1 (){ console.log("promise1") }) }; channel.port2.postMessage(0); setTimeout(function setTimeout2(){ console.log("setTimeout2") }, 0) console.log("sync") }
執行順序:
sync postMessage promise1 setTimeout1 setTimeout2
timelime:
第一個黃塊是onmessage1,第二個是setTimeout1,第三個是setTimeout2。顯而易見,postMessage屬于task,因為setTimeout的4ms標準化了,所以這里的postMessage會優先setTimeout運行。
小結上邊的例子可以得出一些結論:
在一輪event loop中多次修改同一dom,只有最后一次會進行繪制。
渲染更新(Update the rendering)會在event loop中的tasks和microtasks完成后進行,但并不是每輪event loop都會更新渲染,這取決于是否修改了dom和瀏覽器覺得是否有必要在此時立即將新狀態呈現給用戶。如果在一幀的時間內(時間并不確定,因為瀏覽器每秒的幀數總在波動,16.7ms只是估算并不準確)修改了多處dom,瀏覽器可能將變動積攢起來,只進行一次繪制,這是合理的。
如果希望在每輪event loop都即時呈現變動,可以使用requestAnimationFrame。
應用event loop的大致循環過程,可以用下邊的圖表示:
假設現在執行到currently running task,我們對批量的dom進行異步修改,我們將此任務插進task:
此任務插進microtasks:
可以看到如果task隊列如果有大量的任務等待執行時,將dom的變動作為microtasks而不是task能更快的將變化呈現給用戶。
同步簡簡單單就可以完成了,為啥要異步去做這些事?對于一些簡單的場景,同步完全可以勝任,如果得對dom反復修改或者進行大量計算時,使用異步可以作為緩沖,優化性能。
舉個小例子:
現在有一個簡單的元素,用它展示我們的計算結果:
this is result
有一個計算平方的函數,并且會將結果響應到對應的元素
function bar (num, id) { var product = num * num; var resultEle = document.getElementById( id ); resultEle.textContent = product; }
現在我們制造些問題,假設現在很多同步函數引用了bar,在一輪event loop里,可能bar會被調用多次,并且其中有幾個是對id="result"的元素進行操作。就像下邊一樣:
... bar( 2, "result" ) ... bar( 4, "result" ) ... bar( 5, "result" ) ...
似乎這樣的問題也不大,但是當計算變得復雜,操作很多dom的時候,這個問題就不容忽視了。
用我們上邊講的event loop知識,修改一下bar。
var store = {}, flag = false; function bar (num, id) { store[ id ] = num; if(!flag){ Promise.resolve().then(function () { for( var k in store ){ var num = store[k]; var product = num * num; var resultEle = document.getElementById( k ); resultEle.textContent = product; } }); flag = true; } }
現在我們用一個store去存儲參數,統一在microtasks階段執行,過濾了多余的計算,即使同步過程中多次對一個元素修改,也只會響應最后一次。
寫了個簡單插件asyncHelper,可以幫助我們異步的插入task和microtask。
例如:
//生成task var myTask = asyncHelper.task(function () { console.log("this is task") }); //生成microtask var myMicrotask = asyncHelper.mtask(function () { console.log("this is microtask") }); //插入task myTask() //插入microtask myMicrotask();
對之前的例子的使用asyncHelper:
var store = {}; //生成一個microtask var foo = asyncHelper.mtask(function () { for( var k in store ){ var num = store[k]; var product = num * num; var resultEle = document.getElementById( k ); resultEle.textContent = product; } }, {callMode: "last"}); function bar (num, id) { store[ id ] = num; foo(); }
如果不支持microtask將回退成task。
結語event loop涉及到的東西很多,本文有誤的地方請指正。
referenceshttps://jakearchibald.com/201...
https://promisesaplus.com/#notes
https://developers.google.cn/...
http://davidshariff.com/blog/...
http://stackoverflow.com/ques...
https://www.html5rocks.com/zh...
https://vimeo.com/96425312
https://html.spec.whatwg.org/...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/82811.html
摘要:我不該動你的,寫在前面的話本意是想好好研究下,看了幾篇博客后,才意識到作為前端打字員的我有多無知,這坑忒深了。這樣的話,如果是第一種解釋,應該在運行之前,頁面就變成了紅色否則就應該采取第二種解釋。 我不該動你的,Event Loops 寫在前面的話 本意是想好好研究下 Event Loops, 看了幾篇博客后,才意識到作為前端打字員的我有多無知,這坑忒深了。 macrotask?,mi...
摘要:由于個人精力有限,一些技術點的歸納可能有失偏頗,或者目前并未納入進來,因此上的清單內容也會不斷更新。 2018 眼看就要過去了,今年的你相較去年技術上有怎樣的收獲呢? 記得年初的時候我給自己制定了一個學習計劃,現在回顧來看完成度還不錯。但仍有些遺憾,一些技術點沒有時間去好好學習。 在學習中我發現,像文章這樣的知識往往是碎片化的,而前端涉及到的面很多,如果不將這些知識有效梳理,則無法形成...
摘要:由于個人精力有限,一些技術點的歸納可能有失偏頗,或者目前并未納入進來,因此上的清單內容也會不斷更新。 2018 眼看就要過去了,今年的你相較去年技術上有怎樣的收獲呢? 記得年初的時候我給自己制定了一個學習計劃,現在回顧來看完成度還不錯。但仍有些遺憾,一些技術點沒有時間去好好學習。 在學習中我發現,像文章這樣的知識往往是碎片化的,而前端涉及到的面很多,如果不將這些知識有效梳理,則無法形成...
摘要:由于個人精力有限,一些技術點的歸納可能有失偏頗,或者目前并未納入進來,因此上的清單內容也會不斷更新。 2018 眼看就要過去了,今年的你相較去年技術上有怎樣的收獲呢? 記得年初的時候我給自己制定了一個學習計劃,現在回顧來看完成度還不錯。但仍有些遺憾,一些技術點沒有時間去好好學習。 在學習中我發現,像文章這樣的知識往往是碎片化的,而前端涉及到的面很多,如果不將這些知識有效梳理,則無法形成...
閱讀 3338·2022-01-04 14:20
閱讀 3107·2021-09-22 15:08
閱讀 2188·2021-09-03 10:44
閱讀 2315·2019-08-30 15:44
閱讀 1490·2019-08-29 18:40
閱讀 2654·2019-08-29 17:09
閱讀 2988·2019-08-26 13:53
閱讀 3220·2019-08-26 13:37