本文同步發佈在個人 Github博客javascript
最近在研究 React Fiber 相關的知識,上一篇文章 淺談對 React Fiber 的理解 簡單提到了 requestIdleCallback, React 源碼中 polyfill 了這個方法,瞭解它對 Fiber 也能有進一步理解。本篇會深刻介紹下這個方法。前端
requestIdleCallback 是 window 屬性上的方法,它的做用是在瀏覽器一幀的剩餘空閒時間內執行優先度相對較低的任務。java
在網頁運行中,有不少耗時但又不是那麼重要的任務。這些任務和重要的任務如對用戶的輸入做出及時響應的之類的任務,它們共享事件隊列。若是二者發生衝突,用戶體驗會很糟糕。react
requestIdleCallback 就解決了這個痛點,requestIdleCallback 會在每一幀結束時而且有空閒時間執行回調。git
假設須要大量涉及到 DOM 的操做的計算,在運算時,瀏覽器可能就會出現明顯的卡頓行爲,甚至不能進行任何操做,由於是 JS 單線程,就算用在輸入處理,給定幀渲染和合成以後,用戶的主線程就會變得空閒,直到下一幀的開始。github
而這些空閒時間能夠拿來處理低優先級的任務,React16 的調度策略異步可中斷,其中關鍵就靠的這個(polyfill)方法功能;React 把任務細分(時間切片),在瀏覽器空閒的時間去執行,從而儘量地提升渲染性能。redux
時間切片的本質是模擬實現 requestIdleCallback瀏覽器
講到這裏,從 React15 到 React16 Fiber,對總體性能來講是大優化了;但要知道的是,React16 相對 15 作出的優化,並非大大減小了任務量,你寫的代碼的任務總量並無變化,只是把空閒時間利用起來了,不停的幹活,就能更快的把活幹完;這只是其中一個角度,React 還作了區分優先級執行等等。antd
當前大多數的屏幕刷新率都是 60HZ,一秒 60 幀(FPS 爲 60),也就是每秒屏幕刷新 60 次,此時一幀的時間爲 16.7ms(1000ms/60)低於 60HZ 人眼就會感知卡頓掉幀等狀況。異步
瀏覽器的一幀說的就是一次完整的重繪。
前端瀏覽器所說的渲染頻率 FPS(Frames Per Second)是每秒傳輸幀數,便是每秒刷新的次數,理論上 FPS 越高人眼以爲界面越流暢。
window.requestIdleCallback()方法將在瀏覽器的空閒時段內調用的函數排隊。這使開發者可以在主事件循環上執行後臺和低優先級工做,而不會影響延遲關鍵事件,如動畫和輸入響應。函數通常會按先進先調用的順序執行,然而,若是回調函數指定了執行超時時間 timeout,則有可能爲了在超時前執行函數而打亂執行順序。
你能夠在空閒回調函數中調用 requestIdleCallback(),以便在下一次經過事件循環以前調度另外一個回調。
var handle = window.requestIdleCallback(callback[, options])
返回一個 ID 標識符。
callback: 一個在事件循環空閒時即將被調用的函數的引用。函數會接收到一個名爲 deadline 的參數,這個參數能夠獲取當前空閒時間以及回調是否在超時時間前已經執行的狀態;該對象上有兩個屬性: - timeRemaining:timeRemaining屬性是一個函數,函數的返回值表示返回當前空閒時間的剩餘時間 - didTimeout:didTimeout屬性是一個布爾值,若是didTimeout是true,那麼表示本次callback的執行是由於超時的緣由 options 可選 包括可選的配置參數。具備以下屬性: timeout:若是指定了timeout並具備一個正值,而且還沒有經過超時毫秒數調用回調,那麼回調會在下一次空閒時期被強制執行,儘管這樣極可能會對性能形成負面影響。
function myNonEssentialWork(deadline) { while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) { doWorkIfNeeded() } if (tasks.length > 0) { requestIdleCallback(myNonEssentialWork) } } requestIdleCallback(myNonEssentialWork, 5000)
來看一個實際的例子:
requestIdleCallback(myWork) // 一個任務隊列 let tasks = [ function t1() { console.log('執行任務1') }, function t2() { console.log('執行任務2') }, function t3() { console.log('執行任務3') }, ] // deadline是requestIdleCallback返回的一個對象 function myWork(deadline) { console.log(`當前幀剩餘時間: ${deadline.timeRemaining()}`) // 查看當前幀的剩餘時間是否大於0 && 是否還有剩餘任務 if (deadline.timeRemaining() > 0 && tasks.length) { // 在這裏作一些事情 const task = tasks.shift() task() } // 若是還有任務沒有被執行,那就放到下一幀調度中去繼續執行,相似遞歸 if (tasks.length) { requestIdleCallback(myWork) } }
個人運行結果以下(每次運行,不一樣機器運行都不同):
當前幀剩餘時間: 15.120000000000001 執行任務1 當前幀剩餘時間: 15.445000000000002 執行任務2 當前幀剩餘時間: 15.21 執行任務3
若是是由於 timeout 回調才得以執行的話,其實用戶就有可能會感受到卡頓了,由於一幀的執行時間必然已經超過 16ms 了
這個方法理論上可行,但爲何 React 團隊又 polyfill 這個方法呢?
注意:timeRemaining 最大爲 50ms,是有根據研究得出的,便是說人對用戶輸入的 100 毫秒之內的響應一般被認爲是瞬時的,不會被人察覺到。將空閒時間限制在 50ms 內意味着即便在閒置任務開始後當即發生用戶操做,用戶代理仍然有剩餘的 50ms 能夠在其中響應用戶輸入而不會產生用戶可察覺的滯後。
window.requestAnimationFrame() 告訴瀏覽器——你但願執行一個動畫,而且要求瀏覽器在下次重繪以前調用指定的回調函數更新動畫。該方法須要傳入一個回調函數做爲參數,該回調函數會在瀏覽器下一次重繪以前執行
requestAnimationFrame 比起 setTimeout、setInterval 的優點主要有兩點:
固然通常不會用 setTimeout 的,由於偏差很大
window.requestIdleCallback = function (handler) { let startTime = Date.now() return setTimeout(function () { handler({ didTimeout: false, timeRemaining: function () { return Math.max(0, 50.0 - (Date.now() - startTime)) }, }) }, 1) }
React 團隊則是用 requestAnimationFrame 和 postMessage 模擬實現的,本篇就不講了,有興趣的能夠去了解看看。
我近期會維護的開源項目: