react源碼閱讀4 ExpirationTime

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);
}

  我們先跳過首部的變量定義,直接看函數msToExpirationTimemsToExpirationTime接收一個ms,返回ExpirationTime。函數首先進行((ms / UNIT_SIZE) | 0)的計算,我們不來關注msUNIT_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_OFFSETMAGIC_NUMBER_OFFSET31最大整數減去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;

  我們發現在numnum + 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是怎樣被更新上來的,做了哪一些調整。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章