React源码解析笔记---调度更新(一)

归松
2023-12-01

前言:为了搞清楚react到底是个什么样的“框架”以及它的内部机制,因此开始阅读react源码。在阅读源码的时候,为了梳理流程,根据自己的理解以及网上的一些资料做了一些(大致流程的)笔记。笔记是当时的理解,写的时候,仍然有很多不理解的地方待后续完善。而写出来的部分,可能会有很多理解不到位甚至是错误的地方,一旦有新的理解或者发现了错误,会补充与修正。

react源码版本:16.8.6

调度更新

react调度更新主要有几种方式:ReactDom.render、setState、forceUpdate;
ReactDom.render则是通过调用updateContainer去执行更新;后两者则分别通过调用enqueueSetState、enqueueForceUpdate去更新。这三个函数很相似,所以只要看其中一个就可以了。

看一下updateContainer函数源码:

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot, // container, 通过render传过来的FiberRoot实例对象
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  const current = container.current; // Fiber实例对象
  const currentTime = requestCurrentTime(); // 获取currentTime
  const expirationTime = computeExpirationForFiber(currentTime, current);
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    callback,
  );
}

可以看到它做了这几件事:
① 获取了Fiber对象;
② 计算了currentTime;
③ 计算expirationTime;
④ 调用updateContainerAtExpirationTime并取它的返回值返回;

获取currentTime调用了requestCurrentTime这个函数,看一下这个函数的代码:
代码路径:packages/react-reconciler/src/ReactFiberScheduler.js

// Expiration times are computed by adding to the current time (the start
// time). However, if two updates are scheduled within the same event, we
// should treat their start times as simultaneous, even if the actual clock
// time has advanced between the first and second call.

// In other words, because expiration times determine how updates are batched,
// we want all updates of like priority that occur within the same event to
// receive the same expiration time. Otherwise we get tearing.
let currentEventTime: ExpirationTime = NoWork;

export function requestCurrentTime() {
  if (workPhase === RenderPhase || workPhase === CommitPhase) {
    // We're inside React, so it's fine to read the actual time.
    return msToExpirationTime(now());
  }
  // We're not inside React, so we may be in the middle of a browser event.
  if (currentEventTime !== NoWork) {
    // Use the same start time for all updates until we enter React again.
    return currentEventTime;
  }
  // This is the first update since React yielded. Compute a new start time.
  currentEventTime = msToExpirationTime(now());
  return currentEventTime;
}

这里面有三种情况:

  • 如果是处于RenderPhase和CommitPhase阶段,重新获取时间;
  • 如果是不是处于RenderPhase ,CommitPhase阶段,而且currentEventTime不处于NoWork就说明react正在处理浏览器事件,React想让来自同一事件的相同优先级的更新的保持相同的时间,因此直接返回之前的时间;
  • 如果currentEventTime处于NoWork阶段,就说明是初次更新。计算,更新并返回时间;

然后看一下是怎么计算开始时间的,这里用了一个转化函数msToExpirationTime,将毫秒转化为expirationTime,函数参数是now(),看一下这个now是什么,跳转到:packages/react-reconciler/src/SchedulerWithReactIntegration.js

let initialTimeMs: number = Scheduler_now();

// If the initial timestamp is reasonably small, use Scheduler's `now` directly.
// This will be the case for modern browsers that support `performance.now`. In
// older browsers, Scheduler falls back to `Date.now`, which returns a Unix
// timestamp. In that case, subtract the module initialization time to simulate
// the behavior of performance.now and keep our times small enough to fit
// within 32 bits.
// TODO: Consider lifting this into Scheduler.
export const now =
  initialTimeMs < 10000 ? Scheduler_now : () => Scheduler_now() - initialTimeMs;
  

这个initialTimeMs就是模块的初始时间,且初始化了之后就不会变;而这里的Scheduler_now(),会有两种情况,在高版本浏览器中会返回performance.now()的值,在低版本浏览器中会返回Date.now()的值;前者表示从开始时间到调用它所经过的毫秒数,它精确到微秒;后者表示自1970年1月1日 00:00:00 UTC到当前时间的毫秒数。

这里在取当前时间的时候做了一个限制,如果initialTimeMs小于10秒,那么就直接返回初始时间,如果大于10秒,就重新获取当前时间,并减去初始时间。这么做有两个原因:① 是为了让now的值始终小于Math.pow(2, 30) - 1,该值是最大整数限制(查看maxSigned31BitInt.js);② 在低版本浏览器不支持的情况下,模拟performance.now的行为。因此可以看到,now返回的是毫秒数,表示从应用开始到现在经历了多长时间,而且这个值每次获取都比上一次的数值大;

再来看一下msToExpirationTime这个函数:

const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = MAX_SIGNED_31_BIT_INT - 1;

// 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 MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}

这个公式只有一个变量ms,其他都是常量,因此传入的ms越大,msToExpirationTime的返回值越小。


因此通过requestCurrentTime拿到的currentTime只有2种情况:要么比之前大,要么与之前相同。

计算出currentTime之后,就是调用computeExpirationForFiber函数来获取expirationTime了,看一下computeExpirationForFiber的代码:

export function computeExpirationForFiber(
  currentTime: ExpirationTime,
  fiber: Fiber,
): ExpirationTime {
  if ((fiber.mode & ConcurrentMode) === NoContext) {
    return Sync;
  }

  if (workPhase === RenderPhase) {
    // Use whatever time we're already rendering
    return renderExpirationTime;
  }

  // Compute an expiration time based on the Scheduler priority.
  let expirationTime;
  const priorityLevel = getCurrentPriorityLevel();
  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');
  }

  // If we're in the middle of rendering a tree, do not update at the same
  // expiration time that is already rendering.
  if (workInProgressRoot !== null && expirationTime === renderExpirationTime) {
    // This is a trick to move this update into a separate batch
    expirationTime -= 1;
  }

  return expirationTime;
}

这里面有好几个分支,根据workPhase以及优先级分别返回不同的结果。这里面需要看的是computeInteractiveExpiration和computeAsyncExpiration这两个函数,作用分别是计算交互事件的expirationTime和异步任务的expirationTime,来看一下:


代码路径:packages/react-reconciler/src/ReactFiberExpirationTime.js


function ceiling(num: number, precision: number): number {
  return (((num / precision) | 0) + 1) * precision;
}

function computeExpirationBucket(
  currentTime,
  expirationInMs,
  bucketSizeMs,
): ExpirationTime {
  return (
    MAGIC_NUMBER_OFFSET -
    ceiling(
      MAGIC_NUMBER_OFFSET - currentTime + 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;

export function computeInteractiveExpiration(currentTime: ExpirationTime) {
  return computeExpirationBucket(
    currentTime,
    HIGH_PRIORITY_EXPIRATION,
    HIGH_PRIORITY_BATCH_SIZE,
  );
}

可以看到,这两个函数的逻辑其实类似,所以只分析一个。
computeAsyncExpiration函数里面又调用了computeExpirationBucket函数,传入上面计算出来的currentTime以及两个常量。computeExpirationBucket的返回值比较复杂,可以简化一下:

return (
    MAGIC_NUMBER_OFFSET -
    ceiling(
      MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
      bucketSizeMs / UNIT_SIZE,
    )
  );

// 简化
return 1073741822 - ceiling((1073741822 - currentTime + 500), 25);

//进一步简化
1073741822  - ((((1073741822 - currentTime + 500)/ 25) | 0) + 1) * 25;

| 0 的意思是取整(不作四舍五入),所以整个返回值的意思是:取得(1073741822 - currentTime + 500)/ 25(简称A值)的整数部分再加1,最后乘以25。这就有了一个问题,为什么要取整。react这么做,其实是为了合并更新任务,提高效率。如果在一个地方多次调用setState,那么他们的currentTime虽然可能极其接近,但是也不相同,于是计算出来的expirationTime也不同,就要进行不同的更新任务,那么效率就会变低,于是react就让合并比较时间比较接近的更新任务,即:让它们的expirationTime保持一致。

这里可以看到A值的小数部分被去掉了,可以这样说,它除以25之后的余数部分是小于25的。可以假设某一个A值,刚好它可以整除25,那么从A到A+24之间的数,取整拿到的结果都是一样的,因此((((1073741822 - currentTime + 500)/ 25) | 0) + 1) * 25得到的结果也是一样的。这就大大减少了更新任务的触发,也大大提高了效率。


再回到计算不同expirationTime的函数computeExpirationForFiber里,可以看到一共有这几种expirationTime:
a. 0(NoWork)
b. 1(Never)
c. 1073741823(Sync)
d. computeInteractiveExpiration计算结果
e. computeAsyncExpiration计算结果
f. 当前的expirationTime-1

其中d跟e两个依赖于currentTime的变化,currentTime越大,expirationTime越大。

计算完expirationTime,再就是调用updateContainerAtExpirationTime,它主要的流程就是在函数里面调用scheduleRootUpdate,看一下它的源码,这里隐去了一些提示代码:

function scheduleRootUpdate(
  current: Fiber,
  element: ReactNodeList,
  expirationTime: ExpirationTime, // expirationTime就是之前计算得到的结果
  callback: ?Function,
) {
  // createUpdate返回值是一个对象,部分属性:
  // expirationTime: expirationTime,
  // tag: 0 | 1 | 2| 3, 指定的更新类型
  // payload: null, 承载的参数,即更新内容
  // callback: null, 更新之后对应的回调函数,如render,setState对应的回调
  const update = createUpdate(expirationTime); 
  // Caution: React DevTools currently depends on this property
  // being called "element".
  // 第一次渲染,payload为element
  update.payload = {element};
  
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    update.callback = callback;
  }

  flushPassiveEffects();
  // 将更新放入对应Fiber的队列
  enqueueUpdate(current, update);
  // 调度
  scheduleWork(current, expirationTime);

  return expirationTime;
}

enqueueUpdate代码(添加了注释):

// 这个函数有两个大判断
// 前面一个主要是执行createUpdateQueue或者cloneUpdateQueue
// 后面一个判断主要是执行appendUpdateToQueue
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
  // Update queues are created lazily.
  const alternate = fiber.alternante;
  let queue1;
  let queue2;
  if (alternate === null) {
  	// 初次执行渲染
    // There's only one fiber.
    queue1 = fiber.updateQueue;
    queue2 = null;
    if (queue1 === null) {
      // 创建queue
      queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
    }
  } else { 
    // 已经渲染更新过至少一次了
    // else部分主要是判断Fiber对象的updateQueue与alternate对象下的updateQueue对象是否为空
    // 如果都为空,则分别创建,如果有一个为空,则克隆不为空的那一个,如果都不为空,则不做处理
    // There are two owners.
    queue1 = fiber.updateQueue;
    queue2 = alternate.updateQueue;
    if (queue1 === null) {
      if (queue2 === null) {
        // Neither fiber has an update queue. Create new ones.
        queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
        queue2 = alternate.updateQueue = createUpdateQueue(
          alternate.memoizedState,
        );
      } else {
        // Only one fiber has an update queue. Clone to create a new one.
        queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
      }
    } else {
      if (queue2 === null) {
        // Only one fiber has an update queue. Clone to create a new one.
        queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
      } else {
        // Both owners have an update queue.
      }
    }
  }
  
 // 这部分判断的作用主要是将update挂到queue1和queue2中
  if (queue2 === null || queue1 === queue2) {
    // There's only a single queue.
    appendUpdateToQueue(queue1, update);
  } else {
    // There are two queues. We need to append the update to both queues,
    // while accounting for the persistent structure of the list — we don't
    // want the same update to be added multiple times.
    if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
      // One of the queues is not empty. We must add the update to both queues.
      appendUpdateToQueue(queue1, update);
      appendUpdateToQueue(queue2, update);
    } else {
      // Both queues are non-empty. The last update is the same in both lists,
      // because of structural sharing. So, only append to one of the lists.
      appendUpdateToQueue(queue1, update);
      // But we still need to update the `lastUpdate` pointer of queue2.
      queue2.lastUpdate = update;
    }
  }
}

前面的工作都做好了,接下来开始调度…
/scheduleWork
React源码解析笔记—调度更新(二)

 类似资料: