一文吃透 React Expiration Time
Expiration Time 概念
首先 Expiration Time 到底是什麼呢? 根據英文直接翻譯可知,到期時間或者過期時間。在React中到期時間概念又如何理解,我們不妨從它的作用入手理解到底是什麼概念。
Expiration Time 作用
在 React
中,原始碼位置是在 準備階段 updateContainer
的位置 呼叫 computeExpirationForFiber
計算時間,這裡是在準備階段建立好React的更新物件,為後面的後面 React
排程做準備。它代表的是 任務在未來的哪個時間點上應該被執行,不然它就過期了。具體可以檢視 react-reconciler
包中 ReactFiberExpirationTime.js
具體的程式碼內容
總結一下:React
在建立更新的過程 為了後面更新排程的時候,合理安排更新順序,React
會設定一個過期時間(Expiration Time),當 Expiration-Time
到了以後,就會強制更新。
具體原始碼內容
原始碼因為版本不一樣,會有大同小異,這裡不做具體分析
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): ExpirationTime {
// 獲取當前 更新的 Fiber 節點
const current = container.current;
// 獲取當前的時間
const currentTime = requestCurrentTime();
// 計算 ExpirationTime
const expirationTime = computeExpirationForFiber(currentTime, current);
return updateContainerAtExpirationTime(
element,
container,
parentComponent,
expirationTime,
callback,
);
}
複製程式碼
如何計算 Expiration Time
首先我們看 Expiration Time 程式碼,這裡只是涉及到計算方式
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
export type ExpirationTime = number;
export const NoWork = 0;
export const Sync = 1;
export const Never = MAX_SIGNED_31_BIT_INT;
const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = 2;
// 1 unit of expiration time represents 10ms.
export function msToExpirationTime(ms: number): ExpirationTime {
// Always add an offset so that we don't clash with the magic number for NoWork.
return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET;
}
export function expirationTimeToMs(expirationTime: ExpirationTime): number {
return (expirationTime - MAGIC_NUMBER_OFFSET) * UNIT_SIZE;
}
function ceiling(num: number, precision: number): number {
return (((num / precision) | 0) + 1) * precision;
}
// 核心內容
function computeExpirationBucket(
currentTime,
expirationInMs,
bucketSizeMs,
): ExpirationTime {
// currentTime 是當前的時間戳
return (
MAGIC_NUMBER_OFFSET +
ceiling(
currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,
bucketSizeMs / UNIT_SIZE,
)
);
}
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,
);
}
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
// Interactive 型別
export function computeInteractiveExpiration(currentTime: ExpirationTime) {
return computeExpirationBucket(
currentTime,
HIGH_PRIORITY_EXPIRATION,
HIGH_PRIORITY_BATCH_SIZE,
);
}
複製程式碼
看到程式碼可以看到兩種型別的 Expiration Time
一種是 普通非同步的 一種是 Interactive
型別 Interactive
比如說是由事件觸發的,那麼他的響應優先順序會比較高 因為涉及到互動。
舉例&核心內容
我們隨便拿一個型別舉例 computeExpirationBucket
中傳入 currentTime
5000
250
這裡涉及到一個方法 ceiling
可以理解成取整的方法 最終可以得到 ((((currentTime - 2 + 5000 / 10) / 25) | 0) + 1) * 25
其中 25 是 250 / 10, | 0 是取整的作用
公式的含義是什麼呢? 前面 currentTime - 2 + 5000 / 10
這部分是相對固定的內容 等於說是當前時間 + 498
然後 ➗ 25 取整 然後 ➕ 1 再 × 5
最後就是 (當前時間 + 498)➗ 25 取整 然後 ➕ 1 再 × 5
當前時間加上498
然後處以25
取整再加1
再乘以 5,需要注意的是這裡的currentTime
是經過msToExpirationTime
處理的,也就是((now / 10) | 0) + 2
,所以這裡的減去2
可以無視,而除以 10 取整應該是要抹平 10 毫秒內的誤差,當然最終要用來計算時間差的時候會呼叫 expirationTimeToMs
恢復回去,但是被取整去掉的 10 毫秒誤差肯定是回不去的
簡單來說在這裡,最終結果是以25
為單位向上增加的,比如說我們輸入10002 - 10026
之間,最終得到的結果都是10525
,但是到了10027
的到的結果就是10550
,這就是除以25
取整的效果。
另外一個要提的就是msToExpirationTime
和expirationTimeToMs
方法,他們是想換轉換的關係。這裡需要注意有一點非常重要,那就是用來計算expirationTime
的currentTime
是通過msToExpirationTime(now)
得到的,也就是預先處理過的,先處以10
再加了2
這裡的 2 是 magicNumberOffset,所以後面計算expirationTime
要減去2
就可以理解了
單元概念
先上程式碼
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;
複製程式碼
上面提到的 25 就是一個 時間單元 在這個時間單元內計算出來的 Expiration-Time
都是一樣的,React是 為了在同一個時間單元內更新的內容都是用相同的 Expiration-Time
這樣更新會被合併(後面有機會可以分享) 假設如果沒有單元概念的話,這樣每次呼叫建立更新,都沒有優先順序順序,這樣就會浪費效能,影響效率了。 這樣 Expiration-Time
就有了優先順序,方便後續排程更新。
小結
React
這麼設計抹相當於抹平了25ms
內計算過期時間的誤差,這樣做的目的是為了非常詳盡的兩次更新得到相同的 expirationTime
, ,然後在一次更新中完成,相當於一個自動的batchedUpdates
批量更新
以上是 expirationTime
的計算方法。後面二會分享 在原始碼中各個 Expiration-Time
介紹
附加內容
在 React 中我們計算expirationTime
要基於當前得時鐘時間,一般來說我們只需要獲取Date.now
或者performance.now
可以,但是每次獲取一下呢比較消耗效能,所以呢 React 設定了currentRendererTime
來記錄這個值,用於一些不需要重新計算得場景。
但是在 ReactFiberScheduler
中呢又提供了currentSchedulerTime
這個變數,同樣也是記錄這個值的,我們看一下requestCurrentTime
方法的實現。 這裡看註釋就知道為什麼了,直接返回最近的時間
if (isRendering) {
// We're already rendering. Return the most recently read time.
return currentSchedulerTime;
}
複製程式碼
這個isRendering
只有在 performWorkOnRoot
的時候才會被設定為true
,而其本身是一個同步的方法,不存在他執行到一半沒有設定isRendering
為false
的時候就跳出,那麼什麼情況下會在這裡出現新的requestCurrentTime
呢?
- 在生命週期方法中呼叫了
setState
方法 - 需要掛起任務的時候
if (
nextFlushedExpirationTime === NoWork ||
nextFlushedExpirationTime === Never
) {
// If there's no pending work, or if the pending work is offscreen, we can
// read the current time without risk of tearing.
recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;
return currentSchedulerTime;
}
複製程式碼
也就是說在一個batched
更新中,只有第一次建立更新才會重新計算時間,後面的所有更新都會複用第一次建立更新的時候的時間,這個也是為了保證在一個批量更新中產生的同類型的更新只會有相同的過期時間
最後
如果你覺得此文對你有一丁點幫助,點個贊。或者可以加入我的開發交流群:1025263163相互學習,我們會有專業的技術答疑解惑
如果你覺得這篇文章對你有點用的話,麻煩請給我們的開源專案點點star:http://github.crmeb.net/u/defu不勝感激 !
PHP學習手冊:https://doc.crmeb.com
技術交流論壇:https://q.crmeb.com
- 遵循Promises/A 規範,深入分析Promise實現細節 | 通過872測試樣例
- 80 行程式碼實現簡易 RxJS
- 前後端分離專案,如何解決跨域問題?
- springboot中攔截並替換token來簡化身份驗證
- 15 行程式碼在 wangEditor v5 使用數學公式
- Java執行緒池必知必會
- EdgeDB 架構簡析
- TS 型別體操:圖解一個複雜高階型別
- 基於babel的埋點工具簡單實現及思考
- 使用craco對cra專案進行構建優化
- Netty核心概念之ChannelHandler&Pipeline&ChannelHandlerContext
- 理解python非同步程式設計與簡單實現asyncio
- Mycat 作為代理服務端的小知識點
- 一文吃透 React Expiration Time
- 前端模組化詳解
- Java必備主流技術流程圖
- 【建議使用】告別if,Java超好用引數校驗工具類
- MySQL模糊查詢再也不用like %了
- Java 8 的Stream流那麼強大,你知道它的原理嗎
- Vue SEO的四種方案