再談談 Promise, setTimeout, rAF, rIC

歡迎關注個人公衆號睿Talk,獲取我最新的文章:
clipboard.pngjavascript

1、前言

Promise, setTimeout, requestAnimationFrame, requestIdleCallback 這幾個概念相信不少人都很熟悉了,最近在看 React Fiber 源碼的時候又對它們有了更深一層的認識,在此分享一下。下文將用 rAF 表明 requestAnimationFrame, rIC 表明 requestIdleCallbackjava

2、事件循環與幀

事件循環和上面 4 個名詞的基本概念在此再也不囉嗦了,咱們着重看下它們之間的關係。瀏覽器是一個 UI 系統,全部的操做最終都會以頁面的形式展示,而頁面的基本單位是幀。一幀中可能包括的任務有下面幾種類型。segmentfault

clipboard.png

  • events: 點擊事件、鍵盤事件、滾動事件等
  • macro: 宏任務,如 setTimeout
  • micro: 微任務,如 Promise
  • rAF: requestAnimationFrame
  • Layout: CSS 計算,頁面佈局
  • Paint: 頁面繪製
  • rIC: requestIdleCallback

理想狀況下,頁面會以 60 幀每秒的幀率來運行,但實際上每秒繪製多少幀是由多個因素決定的,下面舉一些例子:promise

  • 一個加載完成的靜態頁面,當用戶沒有進行交互的狀況下,頁面不須要重繪,幀率爲 0。
  • 快速滾動頁面的時候,可視區域的內容不斷髮生變化,瀏覽器會盡量快的重繪頁面,理想幀率爲 60。
  • 假設頁面有一個註冊了回調的按鈕,回調執行須要 500 毫秒。當點擊按鈕後再快速滾動頁面,頭 500 毫秒頁面是卡住動不了的,後 500 毫秒會盡量快的重繪頁面,這時候理想幀率爲 30。
  • 當使用 rAF 製做動畫的時候,瀏覽器會盡量快的重繪頁面,桌面瀏覽器多是 60 幀,移動瀏覽器多是 30 幀。

從上面的例子能夠看出,頁面的幀率不是固定的,是會動態變化的。當某一幀的任務佔用大量時間的時候,會影響到下一幀的執行。那麼誰來調節幀率呢?顯然只能依靠瀏覽器自身。做爲開發者的咱們是沒法準確預知回調何時執行的。好比:瀏覽器

function animation() {
   console.log('time: ', +new Date());
   setTimeout(animate, 1000 / 60);
}

animation();

上面的函數假定了瀏覽器以幀率 60 來運行,但當幀率達不到的時候,2 幀之間回調可能執行了屢次,也可能一次都不執行,簡稱掉幀。函數

因此在製做動畫的時候,咱們不能預設瀏覽器的幀率,正確的作法是經過 rAF 註冊回調, 由瀏覽器來控制動畫調用時機:oop

function animation() {
   console.log('time: ', +new Date());
   requestAnimationFrame(animation);
}

animation();

rAF 會保證註冊的回調在下次渲染頁面以前執行,且只會執行一次。另外,當頁面處於不可見狀態時,rAF 會自動中止執行,以節省系統資源。佈局

3、執行順序

Promise, setTimeout , rAFrIC 對應 4 種隊列:微任務隊列、宏任務隊列、animation 隊列和 idle 隊列。性能

  • 微任務隊列會在 JS 運行棧爲空的時候當即執行。
  • animation 隊列會在頁面渲染前執行。
  • 宏任務隊列優先級低於微任務隊列,通常也會比 animation 隊列優先級低,但不是絕對
  • idle 隊列優先級最低,當瀏覽器有空閒時間的時候纔會執行。
setTimeout(()=>console.log('setTimeout'), 0);
Promise.resolve().then(()=>console.log('promise'));
requestAnimationFrame(()=>console.log('animation'));
requestIdleCallback(()=>console.log('idle'));

// 執行結果大多數狀況下是: promise, animation, setTimeout, idle
// 少數狀況是:promise, setTimeout, animation, idle

再來談談空閒時間怎麼理解。假設在 1 秒內有 3 幀須要渲染:優化

clipboard.png

  • 第一幀,因爲宏任務佔用了大量的時間,沒有空閒時間。
  • 第二幀,rAF佔用的時間很少,有大量的空閒時間
  • 第三幀,瀏覽器事件佔用的時間很少,有大量的空閒時間

rAF相似,rIC 的執行時機是由瀏覽器控制的,能更好的保證體驗,優化性能。通常優先級高的任務(如 UI 更新)會放在 rAF 隊列,優先級低的任務(如日誌上傳)會放 rIC

4、隊列特性

在一個事件循環內,各個隊列有如下特性:

  • 宏任務隊列,每次只會執行隊列內的一個任務。
  • 微任務隊列,每次會執行隊列裏的所有任務。假設微任務隊列內有 100 個 Promise,它們會一次過所有執行完。這種狀況下極有可能會致使頁面卡頓。若是在微任務執行過程當中繼續往微任務隊列中添加任務,新添加的任務也會在當前事件循環中執行,很容易形成死循環, 如:
function loop() {
    Promise.resolve().then(loop);
}

loop();
  • animation 隊列,跟微任務隊列有點類似,每次會執行隊列裏的所有任務。但若是在執行過程當中往隊列中添加新的任務,新的任務不會在當前事件循環中執行,而是在下次事件循環中執行。
  • idle 隊列,每次只會執行一個任務。任務完成後會檢查是否還有空閒時間,有的話會繼續執行下一個任務,沒有則等到下次有空閒時間再執行。須要注意的是此隊列中的任務也有可能阻塞頁面,當空閒時間用完後任務不會主動退出。若是任務佔用時間較長,通常會將任務拆分紅多個階段,執行完一個階段後檢查還有沒有空閒時間,有則繼續,無則註冊一個新的 idle 隊列任務,而後退出當前任務。React Fiber 就是用這個機制。但最新版的 React Fiber 已經不用 rIC 了,由於調用的頻率過低,改用 rAF

5、總結

本文介紹了 4 種隊列的執行順序和每一個隊列的特性,它們是:宏任務隊列、微任務隊列、animation 隊列和 idle 隊列。實際應用時能夠根據它們各自的特色分配不一樣的任務。

相關文章
相關標籤/搜索