一文吃透 React Expiration Time

語言: CN / TW / HK

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取整的效果。

另外一個要提的就是msToExpirationTimeexpirationTimeToMs方法,他們是想換轉換的關係。這裡需要注意有一點非常重要,那就是用來計算expirationTimecurrentTime是通過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,而其本身是一個同步的方法,不存在他執行到一半沒有設定isRenderingfalse的時候就跳出,那麼什麼情況下會在這裡出現新的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學習手冊:http://doc.crmeb.com
技術交流論壇:http://q.crmeb.com