【React源码笔记4】- updateContainer

React | 2020-07-05 21:58:14 401次 1次

上篇简述了它大概做的事情,本篇将继续进行展开来说明。

一、currentTime

const currentTime = requestCurrentTimeForUpdate();

export function requestCurrentTimeForUpdate() {
  // 不是render或者commit阶段,插个眼,暂时没明白!
  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
    return msToExpirationTime(now());
  }
  // 更新
  if (currentEventTime !== NoWork) {
    // Use the same start time for all updates until we enter React again.
    return currentEventTime;
  }
  // 初次渲染
  currentEventTime = msToExpirationTime(now());
  return currentEventTime;
}

这里用到了 msToExpirationTime 方法,传入一个当前时间,这个 now 方法先简要看下,其中调用的 Scheduler_now 方法,做了各个平台判断,这里我们只关心浏览器端,高版本浏览器中直接用的是 performance(从开始时间到调用它所经过的毫秒数),低版本中使用 Date.now 实现,目前这个 10000 的判断不确定作用,应该是和最大整数有关系,先可以将这个方法理解为 performance

  /**
     Scheduler_now 
   * 低版本:
   * const initialTime = Date.now();
      getCurrentTime = function() {
        return Date.now() - initialTime;
      };
      高版本:
      performance.now()
   */
let initialTimeMs: number = Scheduler_now();
export const now = initialTimeMs < 10000 ? Scheduler_now : () => Scheduler_now() - initialTimeMs;
export function msToExpirationTime(ms: number): ExpirationTime {
  return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}

MAGIC_NUMBER_OFFSET 就是计算机 1073741823 最大数减去了两次1得到,UNIT_SIZE 默认为 10,或零操作代表了只保留整数部分,比如 ms 的值从 20 - 29 区间,得到的 msToExpirationTime 结果都是一样的,十毫秒内的误差为零。


二、requestCurrentSuspenseConfig

目前值为 null

 const suspenseConfig = requestCurrentSuspenseConfig();


三、computeExpirationForFiber

计算任务的过期时间,其中 current 就是 RootFiber

const expirationTime = computeExpirationForFiber(
    currentTime,
    current,
    suspenseConfig,
  );
export function computeExpirationForFiber(
  currentTime: ExpirationTime,
  fiber: Fiber,
  suspenseConfig: null | SuspenseConfig,
): ExpirationTime {
  const mode = fiber.mode;
  //默认是任务不分片的
  if ((mode & BlockingMode) === NoMode) {
    return Sync;
  }
    //任务优先级定义获取
  const priorityLevel = getCurrentPriorityLevel();
  ...
  let expirationTime;
  if (suspenseConfig !== null) {
    ...
  } else {
    // Compute an expiration time based on the Scheduler priority.
    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 = Idle;
        break;
      default:
        ...
    }
  }
  //渲染时禁止再触发更新
  if (workInProgressRoot !== null && expirationTime === renderExpirationTime) {
    expirationTime -= 1;
  }
  return expirationTime;
}

这个方法后续应该还会再分析,目前先简单了解下,根据不同的渲染模式和任务优先级,触发不同的逻辑。如下两个方法中调用的都是 computeExpirationBucket,参数的区别就是根据不同任务的优先级划分,这里说的优先级分为:

1. 立即执行优先级,立即过期

  • 2. 用户阻塞型优先级,250毫秒后过期

  • 3. 空闲优先级,永不过期,可以在任意空闲时间内执行

  • 4. 普通优先级,5秒后过期

function computeInteractiveExpiration(currentTime: ExpirationTime) {
  return computeExpirationBucket(
    currentTime,
    HIGH_PRIORITY_EXPIRATION, // 150
    HIGH_PRIORITY_BATCH_SIZE, // 100
  );
}
export function computeAsyncExpiration(
  currentTime: ExpirationTime,
): ExpirationTime {
  return computeExpirationBucket(
    currentTime,
    LOW_PRIORITY_EXPIRATION, // 5000
    LOW_PRIORITY_BATCH_SIZE, // 250
  );
}
function computeExpirationBucket(
  currentTime,
  expirationInMs,
  bucketSizeMs,
): ExpirationTime {
  return (
    MAGIC_NUMBER_OFFSET -
    ceiling(
      MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE, //UNIT_SIZE = 10
      bucketSizeMs / UNIT_SIZE,
    )
  );
}
//这个方法似曾相识,也是去除误差用的
function ceiling(num: number, precision: number): number {
  return (((num / precision) | 0) + 1) * precision;
}

其中 ceiling 方法很简单,返回一个最小值为 precision,并按照这个值进行区间划分。难以理解的是上面这个函数的计算,按照低优先级模式先将公式套入简化:

1073741822  - ((((1073741822 - currentTime + 500)/ 25) | 0) + 1) * 25;

这样做为了合并更新,短时间内的多次更新不会立即触发,而是抹平误差后统一的时间点进行更新。ExpirationTime 值越大,优先级相对越高,对于立马过期的任务则需要立即执行(就算浏览器没有空闲时间,阻塞也要执行)。


四、createUpdate

更新对象,会被存储在上一篇介绍的 UpdateQueue 更新队列中。

export function createUpdate(
  expirationTime: ExpirationTime,
  suspenseConfig: null | SuspenseConfig,
): Update<*> {
  let update: Update<*> = {
    expirationTime, //deadline 时间,过期则更新
    suspenseConfig, //suspense 配置

    tag: UpdateState, // 更新类型,UpdateState、ReplaceState、ForceUpdate、CaptureUpdate
    payload: null,   //状态变更函数或新状态本身
    callback: null,  //回调,作用于 fiber.effectTag,并将 callback 作为 side-effects 回调

    next: (null: any), //指向下一个 Update
  };
  update.next = update;
  ...
  return update;
}


五、enqueueUpdate

使用 UpdateQueue 存储 Update 更新队列。更新队列有两条,baseQueue 执行中的更新队列,pendingQueue(即 shared.pending)待执行的更新队列。因为 Update 表现为环状单向链表,baseQueuependingQueue 均存储单向链表的尾节点。

React 是可中断的,先提前了解下,丢弃更新的实现在于将 pendingQueue 复制给 baseQueue, 丢弃之前的 baseQueuecurrent fiberwork-in-progress fiber 均会重置 baseQueue)。

export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
  // 这个 updateQueue 对象就是上一篇中创建的
  const updateQueue = fiber.updateQueue;
  if (updateQueue === null) {
    // Only occurs if the fiber has been unmounted.
    return;
  }

  const sharedQueue = updateQueue.shared;
  const pending = sharedQueue.pending;
  if (pending === null) {
    // 初次渲染 这个 update在创建的时候就next指向自身了,可能是为了后续有更新
    update.next = update;
  } else {
    //更新   插入一个更新节点改变next指向
    update.next = pending.next;
    pending.next = update;
  }
  sharedQueue.pending = update;
  ...
}


六、setState

在上面基本上完成了初始渲染的一些前期准备工作,接下来要做的就是进入 scheduleWork 进行调度,在进入调度分析之前先看下 react 中的更新操作,它和 updateContainer 中做的事情很类似,创建更新,存入队列,然后开始调度:

const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    const fiber = getInstance(inst);
    const currentTime = requestCurrentTimeForUpdate();
    const suspenseConfig = requestCurrentSuspenseConfig();
    const expirationTime = computeExpirationForFiber(
      currentTime,
      fiber,
      suspenseConfig,
    );

    const update = createUpdate(expirationTime, suspenseConfig);
    update.payload = payload;
    ...
    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueReplaceState(inst, payload, callback) {
    ...
    //标识 替换
    update.tag = ReplaceState;
    ...
  },
  enqueueForceUpdate(inst, callback) {
    ...
    ////标识 全量更新
    update.tag = ForceUpdate;
    ...
  },
};

可见更新时做的事情都是一样的,通过不同的标识,下一篇将会进入调度阶段。

1人赞

分享到: