react更新中優先級依賴的標識ExpirationTime。閱讀React包的源碼版本爲16.8.6。
這一章節,讓我們拋棄掉react代碼中的聯繫,單純的來看ExpirationTime以及一些計算方式。
ExpirationTime是什麼。
ExpirationTime是一個數字,你可以在react-reconciler
包下的ReactFiberExpirationTime.js
文件中找到它的定義。
export type ExpirationTime = number;
ExpirationTime在React中有什麼作用。
既然ExpirationTime相關的定義出現在react-reconciler
包之下,說明它的作用肯定是和React調用有關。我們從ReactFiberExpirationTime
函數入手,該函數接收一個ms
,返回一個ExpirationTime
。
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
const MAX_SIGNED_31_BIT_INT = 1073741823
export const NoWork = 0;
export const Never = 1;
// 1073741823 - 1
export const Sync = MAX_SIGNED_31_BIT_INT;
// 1073741823 - 2
export const Batched = Sync - 1;
const UNIT_SIZE = 10;
// // 1073741823 - 3
const MAGIC_NUMBER_OFFSET = Batched - 1;
export function msToExpirationTime(ms: number): ExpirationTime {
// Always add an offset so that we don't clash with the magic number for NoWork.
return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}
我們先跳過首部的變量定義,直接看函數msToExpirationTime
。msToExpirationTime
接收一個ms
,返回ExpirationTime
。函數首先進行((ms / UNIT_SIZE) | 0)
的計算,我們不來關注ms
和UNIT_SIZE
是多少,單純來看這裏的計算邏輯。在另一篇文章中提到過《關於JS中number位(Bit)操作的一些思考》,A | 0
這個操作,在JS中是將A
轉換爲32位的帶符號整數
,在這個公式裏面,可以簡單的理解爲取整。那將ms / UNIT_SIZE
之後取整意味着什麼,我們可以簡單將ms
假設爲100前後的數字,UNIT_SIZE
假設爲10來看一下。
(95 / 10) | 0 = 9;
(100 / 10) | 0 = 10;
(105 / 10) | 0 = 10;
(110 / 10) | 0 = 11;
((ms / UNIT_SIZE) | 0)
這個操作,其實是抹平了ms ~ (ms + UNIT_SIZE - 1)
這個範圍的差值,讓ms ~ (ms + UNIT_SIZE - 1)
通過這個公式都能得到相同的數字。
明白了調用的含義之後,我們順着函數調用來看一下ms
到底是什麼。通過全局搜索msToExpirationTime
,可以發現在react-reconciler/ReactFiberWorkLoop.js
中存在msToExpirationTime
的調用。
export function requestCurrentTime() {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// We're inside React, so it's fine to read the actual time.
return msToExpirationTime(now());
}
// ...省略無關邏輯
}
這裏的now方法忽略到調試等邏輯,可以簡單的理解爲Date.now,即獲得當前的時間戳。到這裏我們可以回頭看一下MAGIC_NUMBER_OFFSET
,MAGIC_NUMBER_OFFSET
是31最大整數減去3的值,我們可以簡單的把它理解爲一個很大的常數整數。聯繫這些,我們可以大致的推斷出ExpirationTime
大體上是個什麼值。
ExpirationTime
是根據當前時間戳,抹平了10ms
與最大整數的一個差值。越在後面的執行,時間戳的值會越大,這就意味着與最大整數的差值會越小,ExpirationTime
會越大。因此,只要存在ExpirationTime a
大於ExpirationTime b
,那麼a
肯定是先於b
的存在。React會對應的先去處理它。
實際上ExpirationTime
與調度的優先級有一個相互對應的關係。
// We intentionally set a higher expiration time for interactive updates in
// dev than in production.
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,
);
}
// TODO: This corresponds to Scheduler's NormalPriority, not LowPriority. Update
// the names to reflect.
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,
);
}
翻看ReactFiberExpirationTime.js
文件,我們可以看到申明瞭一些數字的常量,越是調度優先級靠後的,它的值會越大。高優先級調度常量,React又把這些叫做interactive updates
,交互性的更新。可以看到React在內部對事件進行了一個高地優先級的排列優化。而不管高低優先級,都是調用了一個computeExpirationBucket
方法來對ExpirationTime
的值進行了調整。我們來看一下這個函數。
function computeExpirationBucket(
currentTime,
expirationInMs,
bucketSizeMs,
): ExpirationTime {
return (
MAGIC_NUMBER_OFFSET -
ceiling(
MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
bucketSizeMs / UNIT_SIZE,
)
);
}
function ceiling(num: number, precision: number): number {
return (((num / precision) | 0) + 1) * precision;
}
這個ceiling
函數很有意思,同樣的,我們不關心傳入值,單純代入一些值看看結果。
// 2
ceiling(10, 2); // 12
ceiling(11, 2); // 12
ceiling(12, 2); // 14
ceiling(13, 2); // 14
ceiling(100, 4); // 104
ceiling(101, 4); // 104
export const HIGH_PRIORITY_EXPIRATION = DEV ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
我們發現在num
和num + precision - 1
之間的值,都會被置到num + precision
。比如num
爲100,precision
爲4,那麼100~103的值都會被置爲104
,而104會被置爲108
。所以我們可以明白定義的常量的意義。第二個定義的帶BATCH
字樣的差值,實際上是批量更新時允許的微秒差。如HIGH_PRIORITY_BATCH_SIZE
,實際上就是在高優先調度級的批量更新中,HIGH_PRIORITY_BATCH_SIZE / UNIT_SIZE = 100 / 10 = 10
,偏差在10ms的更新會被調整爲同一個expirationTime
時間,進行批量的相同更新。
現在我們進入computeExpirationBucket
來看一下。
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
computeExpirationBucket(
currentTime,
HIGH_PRIORITY_EXPIRATION,
HIGH_PRIORITY_BATCH_SIZE,
);
function computeExpirationBucket(
currentTime,
expirationInMs,
bucketSizeMs,
): ExpirationTime {
return (
MAGIC_NUMBER_OFFSET -
ceiling(
MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE, // (expirationInMs / UNIT_SIZE = 10)
bucketSizeMs / UNIT_SIZE, // 10
)
);
}
上面分析ceiling
實際上是對第一個參數做一個微量的區間調整,不考慮調整情況下,我們可以把函數簡單的看爲如下.
MAGIC_NUMBER_OFFSET -
ceiling(
MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE, // (expirationInMs / UNIT_SIZE = 10)
bucketSizeMs / UNIT_SIZE, // 10
)
// 簡化
MAGIC_NUMBER_OFFSET - (MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE)
// 去括號
MAGIC_NUMBER_OFFSET - MAGIC_NUMBER_OFFSET + currentTime - expirationInMs / UNIT_SIZE
// 去掉 MAGIC_NUMBER_OFFSET
currentTime - expirationInMs / UNIT_SIZE
可以看到,這個函數本質上就是求得了當前時間和定義毫秒的差值。當優先級調度越高,對應的expirationInMs
的值會越小,其得到的值也就會越大。與上面計算ExpirationTime
值越大優先級越高的邏輯上是相同的。我們全局來查詢一下這兩個函數,看看是在哪裏被用到。
export function computeExpirationForFiber(
currentTime: ExpirationTime,
fiber: Fiber,
suspenseConfig: null | SuspenseConfig,
): ExpirationTime {
// ... 省略邏輯
switch (priorityLevel) {
case ImmediatePriority:
expirationTime = Sync;
break;
case UserBlockingPriority:
// TODO: Rename this to computeUserBlockingExpiration
expirationTime = computeInteractiveExpiration(currentTime);
break;
case NormalPriority:
case LowPriority: // TODO: Handle LowPriority
// TODO: Rename this to... something better.
expirationTime = computeAsyncExpiration(currentTime);
break;
case IdlePriority:
expirationTime = Never;
break;
default:
invariant(false, 'Expected a valid priority level');
}
// 省略無關邏輯
}
現在我們可以回到computeExpirationForFiber
函數中來,明白了fiber節點上的expirationTime
是怎樣被更新上來的,做了哪一些調整。