HTML Standard系列:Event loop、requestIdleCallback 和 requestAnimationFrame

上篇回顧:HTML Standard系列:瀏覽器是如何解析頁面和腳本的javascript

簡介

本文目的

  • 理解 Event Loop 設計目的和執行邏輯及其和渲染的關係
  • 理解 requestIdleCallBack/requestAnimationFrame 設計目的
  • 理解 Event Loop 和 IdleCallBack/AnimationFrame/渲染時機的關係

能夠帶着這些問題閱讀本文前端

  • 爲何須要 Event Loop?
  • Event Loop 和渲染到底有什麼關係?
  • 瀏覽器是如何實現 requestAnimationFrame 的,爲何能夠在 Event Loop 的模型下保持JS動畫的幀數?
  • 瀏覽器是如何實現 requestIdleCallBack 的,爲何 react 選擇它來調度diff任務,它是這麼計算瀏覽器空閒時間的?

Event Loop介紹

在瀏覽器的實現上,諸如渲染任務、JavaScript 腳本執行、User Interaction、網絡處理都跑在同一個線程上,當執行其中一個類型的任務的時候意味着其餘任務的阻塞,爲了有序的對各個任務按照優先級進行執行瀏覽器實現了咱們稱爲 Event Loop 調度流程。java

這種設計模型致使了 JavaScript 天生異步的特色,意味着諸如 Ajax 等瀏覽器接口調用的回調將會發生在 Event Loop 將來的循環中,而不是在阻塞當前 Task。react

與這種模型相對的併發模型,相似 iOS 中渲染和腳本執行也是在同一個線程中,當 iOS 開發人員發起網絡請求的時候,通常經過一個優先級較低的線程去完成請求任務,任務線程完成任務後喚起主線程執行網絡請求後的工做。web

咱們看一段簡單的代碼對比。swift

JavaScript中請求一張圖片URL的表現:瀏覽器

// 將一個網絡請求任務推入Event Loop中的任務隊列中
http.request('some.img').then(
    // 將回調推入任務隊列中排隊,並在網絡任務完成後置位可執行任務,等待執行(這裏暫時忽視微任務和宏任務的區別)
    (imgUrl) => imgElement.src = imgUrl;
)
複製代碼

咱們看看iOS中不阻塞UI的請求一張圖片如何作:網絡

// 將請求任務主動推到額外線程的隊列上
DispatchQueue.global().async {
    // 在額外的線程中同步請求
    let imageData = doRequestStuff()
    // 完成後主動將後續任務推入到主線程中完成。
    DispatchQueue.main.async {
        UIImage(data: imageData)
    }
}
複製代碼

能夠看到二者對於開發者而言最大的區別在於主動和被動,JavaScript的開發者只負責發起請求,隨後的任務調度所有交給了隱藏在幕後的Event Loop,iOS開發者則須要主動維護多線程之間的關係。多線程

requestIdleCallback、requestAnimationFrame介紹

爲何這兩個方法要和 Event Loop 一塊兒講?根據上面的例子,咱們發現 Event Loop 的實現大大簡化UI開發,通常開發者只須要將要執行的任務推入 Event Loop 的隊列上就好了,有 Event Loop 天然不會阻塞UI渲染。架構

Event Loop 雖然優勢不少,但仍是存在必定問題,Event Loop 對多線程開發模型就好了抽象,隱藏了複雜的細節,好比咱們壓根不用管網絡請求在瀏覽器內部是併發請求的,隱藏細節就意味着複雜場景開發者擁有的自由度會下降。

典型的問題的就是 long task,當 event loop 中某個任務執行時間超過了50ms,用戶就可能會感到卡頓;另一個問題就是 evetloop 中任務過多,致使高優先級的任務沒法及時執行(咱們沒法控制任務的優先級);好比Js動畫效果。

瞭解JS定時器同窗應該知道,settimeout 和 setinterval 的定時並不是準確的,考慮以下代碼:

// 咱們期待這個動畫幀數爲20幀
var i = 1;
setInterval(() => {
    element.style.width = `${i++}px`
}, 50)

// 在某些狀況下我插入了一堆任務到隊列中
for(var j = 0; j < 10000, j++) {
    setTimeout(() => {
        doSomeStuff()
    }, 99)
}
複製代碼

顯然這個動畫在要執行第二幀的JS腳本的時候,前面排了10000個任務,而這段腳本排在隊列中10001(實際狀況應該更復雜),雖然瀏覽器老是在執行任務後進行渲染工做,但關鍵的腳本沒有執行,渲染的界面天然仍是原來的,這就形成視覺效果上的卡頓。

因而乎 requestAnimationFrame 就出現了,它的定義就是在瀏覽器下次繪製以前將會執行這個方法的回調,具體瀏覽器如何實現了這個方法,能夠保證咱們的動畫避免被長隊列任務所延遲咱們接下來再講。

// 例子,來自MDN
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);
複製代碼

對於 requestIdleCallback 而言,最出名的應用應該就是 react 了,react 使用了 requestIdleCallback 進行 diff 任務的調度工做,避免了單個 diff 任務耗時過長,而致使界面卡頓的問題,這個方法的回調將在 Event Loop 空閒的時候喚起,並提供瀏覽器接下來可使用的空閒時間(即下一幀渲染以前擁有的時間)。

Event Loop

在 HTML Standard 中是這麼描述 Event loop 的:

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. Each agent has an associated event loop.

咱們每一個瀏覽器界面都有對應的 Event loop,細心的同窗可能看到上面寫的是 evet loops ,咱們每一個界面並不止一個 event loop ,而是有多個 event loop,不一樣的 event loop 管理的方向是徹底不一樣的:

  • window event loop(一般咱們講的都是這個)
  • worker event loop(每一個 worker 線程都有個與之關聯的 event loop)
  • worklet event loop(woklet 是能夠訪問渲染引擎的線程,咱們通常用不到)

咱們接下來說的 event loop 都默認指代 window event loop,後者咱們應用並很少,worker 能夠看做一個不能訪問 dom 的 JavaScript 運行環境,而 worklet 還處於草案階段,主要應用於須要超高性能場景( worklet 中跑的是機器碼而不是 JavaScript )。

所謂的 Event Loop 就是咱們界面從建立到銷燬,瀏覽器中不停執行的一些步驟,以及爲了執行這些步驟而持有的一些固有屬性。

每一個 event loop 持有如下屬性:

  • 多個 task queue( taskqueue 是一個 set 而非 queue),task就是常說的宏任務,具體任務能夠是:Events、Parsing、Callbacks
  • 一個 mircotask queue,也就是微任務隊列

疑問點1:爲何要有多個 task queque?由於瀏覽器能夠爲不一樣的 queque 分配不一樣的優先級,從而優先處理某種類型任務。

疑問點2:爲何 task queue 不是隊列,而是集合?由於瀏覽器老是會挑選可執行的任務去執行,而不是根據進入隊列的時間。

event loop 存在期間將會一直執行下列的步驟:

  • 選取一個有可執行任務的 task queue,而且執行其中最老的任務
  • 執行 microtask,直到 mircrotask queue 爲空
  • update render,檢測是否有渲染機會( rendering opportunity),渲染機會根據物理環境決定(依賴機子性能),若是有渲染機會,瀏覽器便會執行繪製工做
  • 若是接下來沒有要執行的 task/microstask,event loop 將會計算空閒時間。

可見瀏覽器並不是每次tick都會執行繪製工做,而是根據物理環境的實際狀況決定。

好比:某次task插入一個p元素,task結束後並不意味着本次tick會將相應的element繪製到界面上。

爲何有了宏任務還須要微任務?

咱們習慣把宏任務和微任務都理解成 JavaScript 異步執行的一種形式。

事實上只有宏任務是異步的,而微任務是對宏任務代碼執行順序的再分配;宏任務執行完後老是會執行完全部微任務,這種意義上微任務是阻塞主線程的,若是你在某個微任務中不斷建立新的微任務,毫無疑問界面會出現假死。全部微任務的意義在於執行一些老是想在某個任務完成後再執行的代碼。

這時候可能不少小夥伴想到 Promise,我我的認爲 Promise 的控制反轉特性纔是它大放異彩的緣由(大多時候咱們喜歡的是 Promise 的語法、鏈式調用;其實並不關心是不是微任務),而不是由於 Promise.then 是個微任務。

這裏有一些奇怪的點,Promise 的規範是應該屬於 ECMAScript 編寫,本應和 HTML Standard 沒有關係,但由於 Promise 的特殊性,瀏覽器基本是照着 HTML Standard 的規範去實現的 Promise,在 ECMAScript 中 Promise.then 註冊的不叫 microtask 而是稱爲 job。

requestAnimationFrame

講完 Event Loop 彷佛前端開發者壓根沒法把握住渲染前的那一個點,爲了解決這個問題 w3c 定義了 requestAnimationFrame 方法,該方法的回調將會在瀏覽器的下一次繪製前。

調用 requestAnimationFrame,將會將回調推入 animation frame request callback list,而一個非空的 animation frame request callback list,將會使瀏覽器週期性的向 event loop 中添加一個任務去執行 requestAnimationFrame 註冊的回調,這裏的週期沒有指明,但咱們很容易推測和剛剛 event loop 中的渲染時機(rendering opportunity)有關。

直到如今咱們依然沒法看到使用 requestAnimationFrame 相對於 setinterval 構建 JS 動畫的優越性,你們都是週期性向 event loop 推送任務,爲何 requestAnimationFrame 就要更穩定呢?

答案藏在 event loop 中在多個 task queue 之間優先級不一致中,每一個 task 擁有一個 task source 屬性,決定了 task 歸屬到哪一個 task queque,而 task queque 擁有不一樣的執行優先級,顯然由 animation frame request callback list 非空而建立的任務優先級是要高於 timer 的。

animation frame request callback list 中全部的回調函數,將會在一次任務內所有執行,意味着同步的屢次調用 requestAnimationFrame,將會在下一次渲染前的一次任務內按順序所有執行。

requestIdleCallback

在講 requestIdleCallback 以前,咱們先回憶一下 react16 新推出的基於 fiber 架構,在 react16 以前 react 使用 stack reconciler,那 react 聲稱 fiber 解決了什麼問題呢?

首先咱們先看看 stack reconciler 存在什麼問題,依然是 JS 動畫的例子,若是咱們使用 requestAnimationFrame 調製咱們的動畫,若是不存在 long task,動畫的幀數將獲得保證;可是若是某個 task 執行時間超過50ms,沒人能夠保證界面不卡頓;這就是 stack reconciler 的問題,存在單次 diff 時間過長的問題。

而 react 推出 fiber 就是爲了解決這個問題,提升動畫的流暢度,將任務切分到多個幀之間,保證子任務不會出現成爲 long task,提供 fiber 這種核心能力的即是 requestIdleCallback。

調用 requestIdleCallback 方法,將使瀏覽器在空閒時段調用該方法的回調函數。

讓咱們回到 Event Loop 的執行流程,能夠得知在 Event Loop 沒有須要執行的任務的時候會計算空閒時間,空閒時間的計算有兩種狀況。

當存在須要連續渲染的幀,空閒時間將會是幀的頻率減去執行任務時間,再減去執行繪製的時間。

當一段時間內沒有繪製和任務發生的時候,空閒時間將盡量的大,可是不會超過50ms。

50ms這個魔法數字來自大數據的分析,有研究表面高於50ms/幀的畫面會讓人以爲卡頓,因此咱們時常要求當個任務不能過長,就是這個緣由。

一樣調用 requestIdleCallback 方法的回調並不會直接進入 task queque,而是在每輪 event loop 結束以前會計算 idleperiod,若是 idleperiod 大於0,纔會將任務放進隊列中。

提示:idleperiod 的時間除了和渲染頻率有關,還和最近要執行的定時器有着必定的關係,idleperiod 老是會小於下個定時器要執行的時間。

同步的調用屢次 requestIdleCallback,該方法的回調執行可能會分佈在不一樣的幀上,每執行完一次回調,瀏覽器會檢查是否還有剩餘的空閒時間,若是沒有,會將執行控制權交還 event loop,若是有才會繼續執行下一個回調,聽起來是否是和 react fiber 的調度很像。

總結

本文和上一篇這個系統的文章的重點都在於弄清楚整個界面的生命週期和運轉過程,理解繪製和腳本執行直接的關係。

我相信 react 的開發人員若是沒讀過規範,是不能設計出fiber這樣的架構的,這些規範知識提供了高性能 web 開發的理論基礎。

上篇:HTML Standard系列:瀏覽器是如何解析頁面和腳本的

相關文章
相關標籤/搜索