關於 Event Loop 的文章不少,可是有不少只是在講「宏任務」、「微任務」,我先提出幾個問題:css
requestAnimationFrame
在哪一個階段執行,在渲染前仍是後?在 microTask
的前仍是後?requestIdleCallback
在哪一個階段執行?如何去執行?在渲染前仍是後?在 microTask
的前仍是後?resize
、scroll
這些事件是什麼時候去派發的。這些問題並非刻意想刁難你,若是你不知道這些,那你可能並不能在遇到一個動畫需求的時候合理的選擇 requestAnimationFrame
,你可能在作一些需求的時候想到了 requestIdleCallback
,可是你不知道它運行的時機,只是膽戰心驚的去用它,祈禱不要出線上 bug。html
這也是本文想要從規範解讀入手,深挖底層的動機之一。本文會酌情從規範中排除掉一些比較晦澀難懂,或者和主流程不太相關的概念。更詳細的版本也能夠直接去讀這個規範,不過比較費時費力。前端
咱們先依據HTML 官方規範從瀏覽器的事件循環講起,由於剩下的 API 都在這個循環中進行,它是瀏覽器調度任務的基礎。vue
爲了協調事件,用戶交互,腳本,渲染,網絡任務等,瀏覽器必須使用本節中描述的事件循環。git
進入更新渲染階段,判斷是否須要渲染,這裏有一個 rendering opportunity
的概念,也就是說不必定每一輪 event loop 都會對應一次瀏覽 器渲染,要根據屏幕刷新率、頁面性能、頁面是否在後臺運行來共同決定,一般來講這個渲染間隔是固定的。(因此多個 task 極可能在一次渲染之間執行)github
若是知足如下條件,也會跳過渲染:web
map of animation frame callbacks
爲空,也就是幀動畫回調爲空,能夠經過 requestAnimationFrame
來請求幀動畫。若是上述的判斷決定本輪不須要渲染,那麼下面的幾步也不會繼續運行:算法
This step enables the user agent to prevent the steps below from running for other reasons, for example, to ensure certain tasks are executed immediately after each other, with only microtask checkpoints interleaved (and without, e.g., animation frame callbacks interleaved). Concretely, a user agent might wish to coalesce timer callbacks together, with no intermediate rendering updates.
有時候瀏覽器但願兩次「定時器任務」是合併的,他們之間只會穿插着microTask
的執行,而不會穿插屏幕渲染相關的流程(好比requestAnimationFrame
,下面會寫一個例子)。
resize
方法。scroll
方法。requestAnimationFrame
的回調。(後文會詳解)task隊列
和microTask
隊列是否都爲空,若是是的話,則進行 Idle
空閒週期的算法,判斷是否要執行 requestIdleCallback
的回調函數。(後文會詳解)對於resize
和 scroll
來講,並非到了這一步纔去執行滾動和縮放,那豈不是要延遲不少?瀏覽器固然會馬上幫你滾動視圖,根據CSSOM 規範所講,瀏覽器會保存一個 pending scroll event targets
,等到事件循環中的 scroll
這一步,去派發一個事件到對應的目標上,驅動它去執行監聽的回調函數而已。resize
也是同理。segmentfault
能夠在這個流程中仔細看一下「宏任務」、「微任務」、「渲染」之間的關係。api
task
隊列並非咱們想象中的那樣只有一個,根據規範裏的描述:
An event loop has one or more task queues. For example, a user agent could have one task queue for mouse and key events (to which the user interaction task source is associated), and another to which all other task sources are associated. Then, using the freedom granted in the initial step of the event loop processing model, it could give keyboard and mouse events preference over other tasks three-quarters of the time, keeping the interface responsive but not starving other task queues. Note that in this setup, the processing model still enforces that the user agent would never process events from any one task source out of order.
事件循環中可能會有一個或多個任務隊列,這些隊列分別爲了處理:
瀏覽器會在保持任務順序的前提下,可能分配四分之三的優先權給鼠標和鍵盤事件,保證用戶的輸入獲得最高優先級的響應,而剩下的優先級交給其餘 Task
,而且保證不會「餓死」它們。
這個規範也致使 Vue 2.0.0-rc.7 這個版本 nextTick
採用了從微任務 MutationObserver
更換成宏任務 postMessage
而致使了一個 Issue。
目前因爲一些「未知」的緣由,jsfiddle 的案例打不開了。簡單描述一下就是採用了 task
實現的 nextTick
,在用戶持續滾動的狀況下 nextTick
任務被延後了好久纔去執行,致使動畫跟不上滾動了。
迫於無奈,尤大仍是改回了 microTask
去實現 nextTick
,固然目前來講 promise.then
微任務已經比較穩定了,而且 Chrome 也已經實現了 queueMicroTask
這個官方 API。不久的將來,咱們想要調用微任務隊列的話,也能夠節省掉實例化 Promise
在開銷了。
從這個 Issue 的例子中咱們能夠看出,稍微去深刻了解一下規範仍是比較有好處的,以避免在遇到這種比較複雜的 Bug 的時候一臉懵逼。
下面的章節中我們來詳細聊聊 requestIdleCallback
和 requestAnimationFrame
。
如下內容中requestAnimationFrame
簡稱爲rAF
在解讀規範的過程當中,咱們發現 requestAnimationFrame
的回調有兩個特徵:
咱們來分析一下,爲何要在從新渲染前去調用?由於 rAF
是官方推薦的用來作一些流暢動畫所應該使用的 API,作動畫不可避免的會去更改 DOM,而若是在渲染以後再去更改 DOM,那就只能等到下一輪渲染機會的時候才能去繪製出來了,這顯然是不合理的。
rAF
在瀏覽器決定渲染以前給你最後一個機會去改變 DOM 屬性,而後很快在接下來的繪製中幫你呈現出來,因此這是作流暢動畫的不二選擇。下面我用一個 setTimeout
的例子來對比。
假設咱們如今想要快速的讓屏幕上閃爍 紅
、藍
兩種顏色,保證用戶能夠觀察到,若是咱們用 setTimeout
來寫,而且帶着咱們長期的誤解「宏任務之間必定會伴隨着瀏覽器繪製」,那麼你會獲得一個預料以外的結果。
setTimeout(() => { document.body.style.background = "red" setTimeout(() => { document.body.style.background = "blue" }) })
能夠看出這個結果是很是不可控的,若是這兩個 Task
之間正好遇到了瀏覽器認定的渲染機會,那麼它會重繪,不然就不會。因爲這倆宏任務的間隔週期過短了,因此很大機率是不會的。
若是你把延時調整到 17ms
那麼重繪的機率會大不少,畢竟這個是通常狀況下 60fps
的一個指標。可是也會出現不少不繪製的狀況,因此並不穩定。
若是你依賴這個 API 來作動畫,那麼就極可能會形成「掉幀」。
接下來咱們換成 rAF
試試?咱們用一個遞歸函數來模擬 10 次顏色變化的動畫。
let i = 10 let req = () => { i-- requestAnimationFrame(() => { document.body.style.background = "red" requestAnimationFrame(() => { document.body.style.background = "blue" if (i > 0) { req() } }) }) } req()
這裏因爲顏色變化太快,gif
錄製軟件沒辦法截出這麼高幀率的顏色變換,因此各位能夠放到瀏覽器中本身執行一下試試,我這邊直接拋結論,瀏覽器會很是規律的把這 10 組也就是 20 次顏色變化繪製出來,能夠看下 performance 面板記錄的表現:
在第一節解讀規範的時候,第 4 點中提到了,定時器宏任務可能會直接跳過渲染。
按照一些常規的理解來講,宏任務之間理應穿插渲染,而定時器任務就是一個典型的宏任務,看一下如下的代碼:
setTimeout(() => { console.log("sto") requestAnimationFrame(() => console.log("rAF")) }) setTimeout(() => { console.log("sto") requestAnimationFrame(() => console.log("rAF")) }) queueMicrotask(() => console.log("mic")) queueMicrotask(() => console.log("mic"))
從直覺上來看,順序是否是應該是:
mic mic sto rAF sto rAF
呢?也就是每個宏任務以後都緊跟着一次渲染。
實際上不會,瀏覽器會合並這兩個定時器任務:
mic mic sto sto rAF rAF
如下內容中requestIdleCallback
簡稱爲rIC
。
咱們都知道 requestIdleCallback
是瀏覽器提供給咱們的空閒調度算法,關於它的簡介能夠看 MDN 文檔,意圖是讓咱們把一些計算量較大可是又沒那麼緊急的任務放到空閒時間去執行。不要去影響瀏覽器中優先級較高的任務,好比動畫繪製、用戶輸入等等。
React 的時間分片渲染就想要用到這個 API,不過目前瀏覽器支持的不給力,他們是本身去用 postMessage
實現了一套。
首先看一張圖,很精確的描述了這個 API 的意圖:
固然,這種有序的 瀏覽器 -> 用戶 -> 瀏覽器 -> 用戶
的調度基於一個前提,就是咱們要把任務切分紅比較小的片,不能說瀏覽器把空閒時間讓給你了,你去執行一個耗時 10s
的任務,那確定也會把瀏覽器給阻塞住的。這就要求咱們去讀取 rIC
提供給你的 deadline
裏的時間,去動態的安排咱們切分的小任務。瀏覽器信任了你,你也不能辜負它呀。
還有一種狀況,也有可能在幾幀的時間內瀏覽器都是空閒的,並無發生任何影響視圖的操做,它也就不須要去繪製頁面:
這種狀況下爲何仍是會有 50ms
的 deadline
呢?是由於瀏覽器爲了提早應對一些可能會突發的用戶交互操做,好比用戶輸入文字。若是給的時間太長了,你的任務把主線程卡住了,那麼用戶的交互就得不到迴應了。50ms 能夠確保用戶在無感知的延遲下獲得迴應。
MDN 文檔中的幕後任務協做調度 API 介紹的比較清楚,來根據裏面的概念作個小實驗:
屏幕中間有個紅色的方塊,把 MDN 文檔中requestAnimationFrame的範例部分的動畫代碼直接複製過來。
草案中還提到:
rIC
的回調,並且可能會長期的推遲下去。因此若是你須要保證你的任務在必定時間內必定要執行掉,那麼你能夠給 rIC
傳入第二個參數 timeout
。 rIC
的回調函數。因此要謹慎使用,由於它會打斷瀏覽器自己優先級更高的工做。timeRemaining()
函數判斷是否有剩餘時間的時候,若是瀏覽器判斷此時有優先級更高的任務,那麼會動態的把這個值設置爲 0,不然就是用預先設置好的 deadline - now
去計算。timeRemaining()
的計算很是動態,會根據不少因素去決定,因此不要期望這個時間是穩定的。若是我鼠標不作任何動做和交互,直接在控制檯經過 rIC
去打印此次空閒任務的剩餘時間,通常都穩定維持在 49.xx
ms,由於此時瀏覽器沒有什麼優先級更高的任務要去處理。
而若是我不停的滾動瀏覽器,不斷的觸發瀏覽器的從新繪製的話,這個時間就變的很是不穩定了。
經過這個例子,你能夠更加有體感的感覺到什麼樣叫作「繁忙」,什麼樣叫作「空閒」。
這個動畫的例子很簡單,就是利用rAF
在每幀渲染前的回調中把方塊的位置向右移動 10px。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <style> #SomeElementYouWantToAnimate { height: 200px; width: 200px; background: red; } </style> </head> <body> <div id="SomeElementYouWantToAnimate"></div> <script> var start = null var element = document.getElementById("SomeElementYouWantToAnimate") element.style.position = "absolute" function step(timestamp) { if (!start) start = timestamp var progress = timestamp - start element.style.left = Math.min(progress / 10, 200) + "px" if (progress < 2000) { window.requestAnimationFrame(step) } } // 動畫 window.requestAnimationFrame(step) // 空閒調度 window.requestIdleCallback(() => { alert("rIC") }) </script> </body> </html>
注意在最後我加了一個 requestIdleCallback
的函數,回調裏會 alert('rIC')
,來看一下演示效果:
alert
在最開始的時候就執行了,爲何會這樣呢一下,想一下「空閒」的概念,咱們每一幀僅僅是把 left
的值移動了一下,作了這一個簡單的渲染,沒有佔滿空閒時間,因此可能在最開始的時候,瀏覽器就找到機會去調用 rIC
的回調函數了。
咱們簡單的修改一下 step
函數,在裏面加一個很重的任務,1000 次循環打印。
function step(timestamp) { if (!start) start = timestamp var progress = timestamp - start element.style.left = Math.min(progress / 10, 200) + "px" let i = 1000 while (i > 0) { console.log("i", i) i-- } if (progress < 2000) { window.requestAnimationFrame(step) } }
再來看一下它的表現:
其實和咱們預期的同樣,因爲瀏覽器的每一幀都"太忙了",致使它真的就無視咱們的 rIC
函數了。
若是給 rIC
函數加一個 timeout
呢:
// 空閒調度 window.requestIdleCallback( () => { alert("rID") }, { timeout: 500 }, )
瀏覽器會在大概 500ms
的時候,無論有多忙,都去強制執行 rIC
函數,這個機制能夠防止咱們的空閒任務被「餓死」。
經過本文的學習過程,我本身也打破了不少對於 Event Loop 以及 rAF、rIC 函數的固有錯誤認知,經過本文咱們能夠整理出如下的幾個關鍵點。
requestAnimationFrame
在從新渲染屏幕以前執行,很是適合用來作動畫。requestIdleCallback
在渲染屏幕以後執行,而且是否有空執行要看瀏覽器的調度,若是你必定要它在某個時間內執行,請使用 timeout
參數。resize
和scroll
事件其實自帶節流,它只在 Event Loop 的渲染階段去派發事件到 EventTarget
上。另外,本文也是對於規範的解讀,規範裏的一些術語比較晦澀難懂,因此我也結合了一些本身的理解去寫這篇文章,若是有錯誤的地方歡迎各位小夥伴指出。
Vue 源碼詳解之 nextTick:MutationObserver 只是浮雲,microtask 纔是核心!(強烈推薦這篇文章)
1.若是本文對你有幫助,就點個贊支持下吧,你的「贊」是我創做的動力。
2.關注公衆號「前端從進階到入院」便可加我好友,我拉你進「前端進階交流羣」,你們一塊兒共同交流和進步。