在React16的新版本,使用了Fiber從新實現了React的核心算法,帶來了殺手鐗增量更新功能。它有能力將整個更新任務拆分爲一個個小的任務,而且能控制這些任務的執行。 這些功能主要是經過兩個核心的技術來實現的:web
•新的數據結構 fiber算法
•調度器api
這篇文章主要對調度器原理進行解析。瀏覽器
你們都知道 JS 和渲染引擎是一個互斥關係。若是 JS 在執行代碼,那麼渲染引擎工做就會被中止。假如咱們有一個很複雜的複合組件須要從新渲染,那麼調用棧可能會很長。bash
調用棧過長,再加上若是中間進行了複雜的操做,就可能致使長時間阻塞渲染引擎帶來很差的用戶體驗,可能瀏覽器會表現出卡頓、假死的狀況,調度就是來解決這個問題的。數據結構
React 會根據任務的優先級去分配各自的 expirationTime,在過時時間到來以前先去處理更高優先級的任務,而且高優先級的任務還能夠打斷低優先級的任務(所以會形成某些生命週期函數屢次被執行),從而實如今不影響用戶體驗的狀況下去分段計算更新(也就是時間分片)。函數
React主要由兩部分實現:oop
一、計算任務的 expriationTimepost
二、實現 requestIdleCallback 的 polyfill 版本性能
expriationTime
expriationTime這個時間是用於幫助咱們對比不一樣任務之間的優先級和計算任務的timeout(是否過時)。
計算公式: expriationTime=當前時間+一個常量(根據任務優先級改變)
當前時間指的是 performance.now(),這個 API 會返回一個精確到毫秒級別的時間戳(固然也並非高精度的),另外瀏覽器也並非全部都兼容 performance API 的。若是使用 Date.now() 的話那麼精度會更差,可是爲了方便起見,咱們這裏統一把當前時間認爲是 performance.now()。
常量指的是根據不一樣優先級得出的一個數值,React 內部目前總共有五種優先級,數值越小優先級越高,分別爲:
var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;
複製代碼
它們各自的對應的數值都是不一樣的,具體的內容以下
var maxSigned31BitInt = 1073741823;
// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var
IDLE_PRIORITY = maxSigned31BitInt;
複製代碼
也就是說,假設當前時間爲 5000 而且分別有兩個優先級不一樣的任務要執行。前者屬於 ImmediatePriority,後者屬於 UserBlockingPriority,那麼兩個任務計算出來的時間分別爲 4999 和 5250(值越小就要優先執行)。經過這個時間能夠比對大小得出誰的優先級高,也能夠經過減去當前時間獲取任務的 timeout。
requestIdleCallback
requestIdleCallback是一個web api接口,它會在瀏覽器空閒時期依次調用函數, 這就可讓開發者在主事件循環中執行後臺或低優先級的任務,並且不會對像動畫和用戶交互這樣延遲敏感的事件產生影響。函數通常會按先進先調用的順序執行,然而,若是回調函數指定了執行超時時間timeout,則有可能爲了在超時前執行函數而打亂執行順序。如timeout 值被指定爲正數時,當作瀏覽器調用 callback 的最後期限。它的單位是毫秒。當指定的時間過去後回調尚未被執行,那麼回調會在下一次空閒時期被強制執行,儘管可能會對性能形成負面影響。
可是requestIdleCallback有一個致命的缺陷,它只能一秒調用回調 20次,這個知足不了現有的狀況,因此React團隊是本身實現這個函數。
實現 requestIdleCallback要點
實現requestIdleCallback主要是實現屢次在瀏覽器空閒時且是渲染後才調用回調方法。
屢次執行可使用requestAnimationFrame,由於它是在瀏覽器的每一幀的重繪以前能夠執行傳入的函數,所以會比較準確。而在主流的瀏覽器中,瀏覽器刷新頻率是60赫茲,一秒鐘60次,就是一次耗時大概16毫秒。
如何判斷瀏覽器當前是否處於空閒? 你們都知道在一幀當中,瀏覽器可能會響應用戶的交互事件、執行 JS、進行渲染的一系列計算繪製。若是以上這些操做超過了 16ms,那麼就會致使這一幀渲染沒有完成並出現掉幀的狀況,會形成頁面有明顯的卡頓,繼而影響用戶體驗;若是以上這些操做沒有耗時 16ms的話,那麼咱們就認爲當下存在空閒時間讓咱們能夠去執行任務。
計算方法見參考文獻。
簡單來講就是假設當前時間爲 5000,瀏覽器支持 60 幀,那麼 1 幀近似 16 毫秒,那麼就會計算出下一幀時間爲 5016。
得出下一幀時間之後,咱們只需對比當前時間是否小於下一幀時間便可,這樣就能清楚地知道是否還有空閒時間去執行任務。
最後,把咱們須要在在渲染之後纔去執行任務生成爲一個宏任務,由於根據event loop是執行一個宏任務,再執行一個隊列的微任務,由於放在宏任務是最合適。爲了能夠最快完成任務,放在MessageChannel來完成這個任務。
首先每一個任務都會有各自的優先級,經過當前時間加上優先級所對應的常量咱們能夠計算出 expriationTime,高優先級的任務會打斷低優先級任務
在調度以前,判斷當前任務是否過時,過時的話無須調度,直接調用 port.postMessage(undefined),這樣就能在渲染後立刻執行過時任務了
若是任務沒有過時,就經過 requestAnimationFrame 啓動定時器,在重繪前調用回調方法
在回調方法中咱們首先須要計算每一幀的時間以及下一幀的時間,而後執行 port.postMessage(undefined)
channel.port1.onmessage 會在渲染後被調用,在這個過程當中咱們首先須要去判斷當前時間是否小於下一幀時間。若是小於的話就表明咱們尚有空餘時間去執行任務;若是大於的話就表明當前幀已經沒有空閒時間了,這時候咱們須要去判斷是否有任務過時,過時的話無論三七二十一仍是得去執行這個任務。若是沒有過時的話,當前幀又沒有時間,那就只能把這個任務丟到下一幀看能不能執行了
本文整體參考自yck,juejin.im/post/5cef53…
在原來的基礎上加入了本身的理解