NodeNote,持續更新中react相關庫源碼淺析, react ts3 項目react
[TOC]git
到期時間算法源碼位置:github
react\packages\react-reconciler\src\ReactFiberExpirationTime.js
複製代碼
摘要: react主要把到期時間分爲兩種:異步任務到期時間與交互動做的到期時間。在這以前須要瞭解一下一些重要的函數,react的到期時間與系統的時間ms不是1:1的關係,低優先級異步任務的兩個時間間隔相差不到250ms(至關於25個單位的 到期時間)的任務會被設置爲同一個到期時間,交互 異步任務間隔爲100ms(10個單位到期時間),所以減小了一些沒必要要的組件渲染,而且保證交互能夠及時的響應。算法
//向上取整,間隔在precision內的兩個num最終獲得的相同的值
function ceiling(num: number, precision: number): number {
return (((num / precision) | 0) + 1) * precision;
}
//根據到期時間與單位爲ms的時間之間的轉換關係,定製ceiling來獲得到期時間
const UNIT_SIZE = 10;// 過時時間單元(ms)
const MAGIC_NUMBER_OFFSET = MAX_SIGNED_31_BIT_INT - 1;// 到期時間偏移量
function computeExpirationBucket(
currentTime,
expirationInMs,
bucketSizeMs,
): ExpirationTime {
return (
MAGIC_NUMBER_OFFSET -
ceiling(
MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
bucketSizeMs / UNIT_SIZE,
)
);
}
複製代碼
ceiling的做用:向上取整,間隔在precision內的兩個num最終獲得的相同的值。 若是precision爲25,則50和66轉換後的到期時間都是75bash
computeExpirationBucket的做用:第一參數是須要轉換的當前時間(單位:單位到期時間 = UNIT_SIZE ms),第二個參數是不一樣優先級的異步任務對應的偏移時間(單位:ms),第三個參數是步進時間(單位:ms)。該函數經過定製ceiling獲得特定單位(10ms一個單位)的到期時間,這個時間對應不一樣優先級的異步任務到期時間。好比:若是是低優先級的異步任務,則第二個參數傳入LOW_PRIORITY_EXPIRATION = 5000
。異步
因爲到期時間越大,優先級越高,所以第二個參數的巧妙之處是,避免在一個當前
時間左右的不一樣優先級任務的到期時間相差無幾,失去了到期時間的意義。
複製代碼
低優先級異步任務到期時間:computeAsyncExpiration函數
//異步任務的到期時間
export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;
//計算異步到期時間
export function computeAsyncExpiration(
currentTime: ExpirationTime,
): ExpirationTime {
return computeExpirationBucket(
currentTime,
LOW_PRIORITY_EXPIRATION,
LOW_PRIORITY_BATCH_SIZE,
);
}
複製代碼
上面currentTime
爲50
和58
轉換後的到期時間都是1073742275
:性能
50: ((1073741822-50+500)/25|0+1)*25 = 1073742275
58: ((1073741822-58+500)/25|0+1)*25 = 1073742275
複製代碼
交互動做的到期時間:computeInteractiveExpirationui
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
export function computeInteractiveExpiration(currentTime: ExpirationTime) {
return computeExpirationBucket(
currentTime,
HIGH_PRIORITY_EXPIRATION,
HIGH_PRIORITY_BATCH_SIZE,
);
}
複製代碼
上面currentTime
爲50
和58
轉換後的到期時間相等,交互任務在開發環境獲得的到期時間大於生產環境:spa
開發環境:
50: ((1073741822-50+50)/10|0+1)*10 = 1073741830
58: ((1073741822-58+50)/10|0+1)*10 = 1073741830
生產環境:
50: ((1073741822-50+15)/10|0+1)*10 = 1073741790
58: ((1073741822-58+15)/10|0+1)*10 = 1073741790
複製代碼
function requestCurrentTime() {
if (isRendering) {
return currentSchedulerTime;
}
findHighestPriorityRoot();
if (
nextFlushedExpirationTime === NoWork ||
nextFlushedExpirationTime === Never
) {
recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;
return currentSchedulerTime;
}
return currentSchedulerTime;
}
複製代碼
在 React 中咱們計算expirationTime要基於當前得時鐘時間,通常來講咱們只須要獲取Date.now或者performance.now就能夠了,可是每次獲取一下呢比較消耗性能,因此 React 設置了currentRendererTime來記錄這個值,用於一些不須要從新計算得場景。
在\react\packages\react-reconciler\src\ReactFiberScheduler.js
中能夠發現以下代碼是同時出現的,先獲取到當前時間賦值給currentRendererTime
,而後currentRendererTime
賦值給currentSchedulerTime
:
recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;
複製代碼
if (isRendering) {
return currentSchedulerTime;
}
複製代碼
isRendering
會在performWorkOnRoot
的開始設置爲true,在結束設置爲false,都是同步的。performWorkOnRoot
的先進入渲染階段而後進入提交階段,react全部的生命週期鉤子都是在此執行的。
在一個事件回調函數中調用屢次setState
的時候,isRendering
老是false
,若是是在生命週期鉤子函數componentDidMount
中調用setState的時候,isRendering
爲true
,由於該鉤子觸發的時機就是在performWorkOnRoot
中。
findHighestPriorityRoot
會找到root雙向鏈表(React.render會建立一個root並添加到這個雙向鏈表中)中有任務須要執行而且到期時間最大即優先級最高的任務,而後將這個須要更新的root以及最大到期時間賦值給nextFlushedRoot
以及nextFlushedExpirationTime
。當沒有任務的時候nextFlushedExpirationTime
爲NoWork
。
if (
nextFlushedExpirationTime === NoWork ||
nextFlushedExpirationTime === Never
) {
recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;
return currentSchedulerTime;
}
複製代碼
若是沒有任務須要執行,那麼從新計算當前時間,並返回,在事件處理函數中第一個setState
會從新計算當前時間,可是第二個setState
的時候,因爲已經有更新任務在隊列中了,因此這裏直接跳過判斷,最後返回上一次setState
時的記錄的當前時間。
注意:這裏調用的recomputeCurrentRendererTime
是經過調用performance.now()或者Date.now()獲取的時間。
return currentSchedulerTime;
複製代碼
返回currentSchedulerTime
。
經過一些標誌來判斷當前fiber發生的更新是處於什麼階段,來計算相應的到期時間。
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
let expirationTime;
if (expirationContext !== NoWork) {
expirationTime = expirationContext;
} else if (isWorking) {
//在renderRoot與commitRoot階段isWorking = true,結束以後都會是false
// 下面就判斷是哪一個階段
if (isCommitting) {
expirationTime = Sync;
} else {
expirationTime = nextRenderExpirationTime;
}
} else {
if (fiber.mode & ConcurrentMode) {
if (isBatchingInteractiveUpdates) {
expirationTime = computeInteractiveExpiration(currentTime);
} else {
expirationTime = computeAsyncExpiration(currentTime);
}
if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
expirationTime -= 1;
}
} else {
expirationTime = Sync;
}
}
if (isBatchingInteractiveUpdates) {
if (
lowestPriorityPendingInteractiveExpirationTime === NoWork ||
expirationTime < lowestPriorityPendingInteractiveExpirationTime
) {
lowestPriorityPendingInteractiveExpirationTime = expirationTime;
}
}
return expirationTime;
}
複製代碼
主要判斷的流程以下:
注意:同步Sync優先級最高
若是context有更新任務須要執行
expirationTime設置爲context上的到期時間
若是處於renderRoot渲染階段或者commitRoot提交階段
若是處於commitRoot
expirationTime設置爲同步Sync
不然(處於renderRoot)
expirationTime設置爲當前的到期時間nextRenderExpirationTime
不然
若是不是Concurrent模式
若是正在批處理交互式更新
利用computeInteractiveExpiration計算expirationTime
不然
利用computeAsyncExpiration計算expirationTime
若是有下一root樹須要更新,而且到期時間與該樹到期時間相等
expirationTime減一,表示讓下一個root先更新
不然
expirationTime設置爲同步Sync
若是正在批處理交互式更新
若是最低優先級的交互式更新優先級大於到期時間expirationTime或者沒有交互式更新任務
將最低優先級的交互式更新任務到期時間設置爲到期時間expirationTime
最後返回expirationTime
複製代碼
同一個事件,同一個生命週期中的setState具有相同的到期時間,所以也存在了多個setState合併的結果。