翼度科技»论坛 编程开发 JavaScript 查看内容

React render核心阶段深入探究穿插scheduler与reconciler

7

主题

7

帖子

21

积分

新手上路

Rank: 1

积分
21
本章将讲解 react 的核心阶段之一 —— render阶段,我们将探究以下部分内容的源码:

  • 更新任务的触发
  • 更新任务的创建
  • reconciler 过程同步和异步遍历及执行任务
  • scheduler 是如何实现帧空闲时间调度任务以及中断任务的

触发更新

触发更新的方式主要有以下几种:
  1. ReactDOM.render
复制代码
  1. setState
复制代码
  1. forUpdate
复制代码
以及 hooks 中的
  1. useState
复制代码
等,关于 hooks 的我们后面再详细讲解,这里先关注前三种情况。

ReactDOM.render
  1. ReactDOM.render
复制代码
作为 react 应用程序的入口函数,在页面首次渲染时便会触发,页面 dom 的首次创建,也属于触发 react 更新的一种情况。
首先调用
  1. legacyRenderSubtreeIntoContainer
复制代码
函数,校验根节点 root 是否存在,若不存在,调用
  1. legacyCreateRootFromDOMContainer
复制代码
创建根节点 root、rootFiber 和 fiberRoot 并绑定它们之间的引用关系,然后调用
  1. updateContainer
复制代码
去非批量执行后面的更新流程;若存在,直接调用
  1. updateContainer
复制代码
去批量执行后面的更新流程:
  1. // packages/react-dom/src/client/ReactDOMLegacy.js
  2. function legacyRenderSubtreeIntoContainer(
  3.   parentComponent: ?React$Component<any, any>,  children: ReactNodeList,  container: Container,  forceHydrate: boolean,  callback: ?Function,
  4. ) {
  5.   // ...
  6.   let root: RootType = (container._reactRootContainer: any);
  7.   let fiberRoot;
  8.   if (!root) {
  9.     // 首次渲染时根节点不存在
  10.     // 通过 legacyCreateRootFromDOMContainer 创建根节点、fiberRoot 和 rootFiber
  11.     root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
  12.       container,
  13.       forceHydrate,
  14.     );
  15.     fiberRoot = root._internalRoot;
  16.     if (typeof callback === 'function') {
  17.       const originalCallback = callback;
  18.       callback = function() {
  19.         const instance = getPublicRootInstance(fiberRoot);
  20.         originalCallback.call(instance);
  21.       };
  22.     }
  23.     // 非批量执行更新流程
  24.     unbatchedUpdates(() => {
  25.       updateContainer(children, fiberRoot, parentComponent, callback);
  26.     });
  27.   } else {
  28.     fiberRoot = root._internalRoot;
  29.     if (typeof callback === 'function') {
  30.       const originalCallback = callback;
  31.       callback = function() {
  32.         const instance = getPublicRootInstance(fiberRoot);
  33.         originalCallback.call(instance);
  34.       };
  35.     }
  36.     // 批量执行更新流程
  37.     updateContainer(children, fiberRoot, parentComponent, callback);
  38.   }
  39.   return getPublicRootInstance(fiberRoot);
  40. }
复制代码
  1. updateContainer
复制代码
函数中,主要做了以下几件事情:

  • requestEventTime:获取更新触发的时间
  • requestUpdateLane:获取当前任务优先级
  • createUpdate:创建更新
  • enqueueUpdate:将任务推进更新队列
  • scheduleUpdateOnFiber:调度更新
关于这几个函数稍后会详细讲到
  1. // packages/react-dom/src/client/ReactDOMLegacy.js
  2. export function updateContainer(
  3.   element: ReactNodeList,  container: OpaqueRoot,  parentComponent: ?React$Component<any, any>,  callback: ?Function,
  4. ): Lane {
  5.   // ...
  6.   const current = container.current;
  7.   const eventTime = requestEventTime(); // 获取更新触发的时间
  8.   // ...
  9.   const lane = requestUpdateLane(current); // 获取任务优先级
  10.   if (enableSchedulingProfiler) {
  11.     markRenderScheduled(lane);
  12.   }
  13.   const context = getContextForSubtree(parentComponent);
  14.   if (container.context === null) {
  15.     container.context = context;
  16.   } else {
  17.     container.pendingContext = context;
  18.   }
  19.   // ...
  20.   const update = createUpdate(eventTime, lane); // 创建更新任务
  21.   update.payload = {element};
  22.   callback = callback === undefined ? null : callback;
  23.   if (callback !== null) {
  24.     // ...
  25.     update.callback = callback;
  26.   }
  27.   enqueueUpdate(current, update); // 将任务推入更新队列
  28.   scheduleUpdateOnFiber(current, lane, eventTime); // schedule 进行调度
  29.   return lane;
  30. }
复制代码
setState

setState 时类组件中我们最常用的修改状态的方法,状态修改会触发更新流程。
class 组件在原型链上定义了
  1. setState
复制代码
方法,其调用了触发器
  1. updater
复制代码
上的
  1. enqueueSetState
复制代码
方法:
  1. // packages/react/src/ReactBaseClasses.js
  2. Component.prototype.setState = function(partialState, callback) {
  3.   invariant(
  4.     typeof partialState === 'object' ||
  5.       typeof partialState === 'function' ||
  6.       partialState == null,
  7.     'setState(...): takes an object of state variables to update or a ' +
  8.       'function which returns an object of state variables.',
  9.   );
  10.   this.updater.enqueueSetState(this, partialState, callback, 'setState');
  11. };
复制代码
然后我们再来看以下 updater 上定义的
  1. enqueueSetState
复制代码
方法,一看到这我们就了然了,和
  1. updateContainer
复制代码
方法中做的事情几乎一模一样,都是触发后续的更新调度。
  1. // packages/react-reconciler/src/ReactFiberClassComponent.old.js
  2. const classComponentUpdater = {
  3.   isMounted,
  4.   enqueueSetState(inst, payload, callback) {
  5.     const fiber = getInstance(inst);
  6.     const eventTime = requestEventTime(); // 获取更新触发的时间
  7.     const lane = requestUpdateLane(fiber); // 获取任务优先级
  8.     const update = createUpdate(eventTime, lane); // 创建更新任务
  9.     update.payload = payload;
  10.     if (callback !== undefined && callback !== null) {
  11.       if (__DEV__) {
  12.         warnOnInvalidCallback(callback, 'setState');
  13.       }
  14.       update.callback = callback;
  15.     }
  16.     enqueueUpdate(fiber, update); // 将任务推入更新队列
  17.     scheduleUpdateOnFiber(fiber, lane, eventTime); // schedule 进行调度
  18.     // ...
  19.     if (enableSchedulingProfiler) {
  20.       markStateUpdateScheduled(fiber, lane);
  21.     }
  22.   },
  23.   // ...
  24. };
复制代码
forceUpdate
  1. forceUpdate
复制代码
的流程与
  1. setState
复制代码
几乎一模一样:

同样其调用了触发器 updater 上的
  1. enqueueForceUpdate
复制代码
方法,
  1. enqueueForceUpdate
复制代码
方法也同样是触发了一系列的更新流程:相关参考视频讲解:传送门
  1. reconciler/src/ReactFiberClassComponent.old.js
  2. const classComponentUpdater = {
  3.   isMounted,
  4.   // ...
  5.   enqueueForceUpdate(inst, callback) {
  6.     const fiber = getInstance(inst);
  7.     const eventTime = requestEventTime(); // 获取更新触发的时间
  8.     const lane = requestUpdateLane(fiber); // 获取任务优先级
  9.     const update = createUpdate(eventTime, lane); // 创建更新
  10.     update.tag = ForceUpdate;
  11.     if (callback !== undefined && callback !== null) {
  12.       if (__DEV__) {
  13.         warnOnInvalidCallback(callback, 'forceUpdate');
  14.       }
  15.       update.callback = callback;
  16.     }
  17.     enqueueUpdate(fiber, update); // 将任务推进更新队列
  18.     scheduleUpdateOnFiber(fiber, lane, eventTime); // 触发更新调度
  19.     // ...
  20.     if (enableSchedulingProfiler) {
  21.       markForceUpdateScheduled(fiber, lane);
  22.     }
  23.   },
  24. };
复制代码
创建更新任务

可以发现,上述的三种触发更新的动作,最后殊途同归,都会走上述流程图中从
  1. requestEventTime
复制代码
  1. scheduleUpdateOnFiber
复制代码
这一流程,去创建更新任务,先我们详细看下更新任务是如何创建的。

获取更新触发时间

前面的文章中我们讲到过,react 执行更新过程中,会将更新任务拆解,每一帧优先执行高优先级的任务,从而保证用户体验的流畅。那么即使对于同样优先级的任务,在任务多的情况下该优先执行哪一些呢?
react 通过
  1. requestEventTime
复制代码
方法去创建一个 currentEventTime,用于标识更新任务触发的时间,对于相同时间的任务,会批量去执行。同样优先级的任务,currentEventTime 值越小,就会越早执行。
我们看一下
  1. requestEventTime
复制代码
方法的实现:
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. export function requestEventTime() {
  3.   if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
  4.     // 在 react 执行过程中,直接返回当前时间
  5.     return now();
  6.   }
  7.   // 如果不在 react 执行过程中
  8.   if (currentEventTime !== NoTimestamp) {
  9.     // 正在执行浏览器事件,返回上次的 currentEventTime
  10.     return currentEventTime;
  11.   }
  12.   // react 中断后首次更新,计算新的 currentEventTime
  13.   currentEventTime = now();
  14.   return currentEventTime;
  15. }
复制代码
在这个方法中,
  1. (executionContext & (RenderContext | CommitContext)
复制代码
做了二进制运算,
  1. RenderContext
复制代码
代表着 react 正在计算更新,
  1. CommitContext
复制代码
代表着 react 正在提交更新。所以这句话是判断当前 react 是否处在计算或者提交更新的阶段,如果是则直接返回
  1. now()
复制代码
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. export const NoContext = /*             */ 0b0000000;
  3. const BatchedContext = /*               */ 0b0000001;
  4. const EventContext = /*                 */ 0b0000010;
  5. const DiscreteEventContext = /*         */ 0b0000100;
  6. const LegacyUnbatchedContext = /*       */ 0b0001000;
  7. const RenderContext = /*                */ 0b0010000;
  8. const CommitContext = /*                */ 0b0100000;
  9. export const RetryAfterError = /*       */ 0b1000000;
  10. let executionContext: ExecutionContext = NoContext;
复制代码
再来看一下
  1. now
复制代码
的代码,这里的意思时,当前后的更新任务时间差小于 10ms 时,直接采用上次的
  1. Scheduler_now
复制代码
,这样可以抹平 10ms 内更新任务的时间差, 有利于批量更新:
  1. // packages/react-reconciler/src/SchedulerWithReactIntegration.old.js
  2. export const now =
  3.   initialTimeMs < 10000 ? Scheduler_now : () => Scheduler_now() - initialTimeMs;
复制代码
综上所述,
  1. requestEvent
复制代码
做的事情如下:

  • 在 react 的 render 和 commit 阶段我们直接获取更新任务的触发时间,并抹平相差 10ms 以内的更新任务以便于批量执行。
  • 当 currentEventTime 不等于 NoTimestamp 时,则判断其正在执行浏览器事件,react 想要同样优先级的更新任务保持相同的时间,所以直接返回上次的 currentEventTime
  • 如果是 react 上次中断之后的首次更新,那么给 currentEventTime 赋一个新的值

划分更新任务优先级

说完了相同优先级任务的触发时间,那么任务的优先级又是如何划分的呢?这里就要提到
  1. requestUpdateLane
复制代码
,我们来看一下其源码:
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. export function requestUpdateLane(fiber: Fiber): Lane {
  3.   // ...
  4.   // 根据记录下的事件的优先级,获取任务调度的优先级
  5.   const schedulerPriority = getCurrentPriorityLevel();
  6.   // ...
  7.   let lane;
  8.   if (
  9.     (executionContext & DiscreteEventContext) !== NoContext &&
  10.     schedulerPriority === UserBlockingSchedulerPriority
  11.   ) {
  12.     // 如果是用户阻塞级别的事件,则通过InputDiscreteLanePriority 计算更新的优先级 lane
  13.     lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes);
  14.   } else {
  15.     // 否则依据事件的优先级计算 schedulerLanePriority
  16.     const schedulerLanePriority = schedulerPriorityToLanePriority(
  17.       schedulerPriority,
  18.     );
  19.     if (decoupleUpdatePriorityFromScheduler) {
  20.       const currentUpdateLanePriority = getCurrentUpdateLanePriority();
  21.     // 根据计算得到的 schedulerLanePriority,计算更新的优先级 lane
  22.     lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
  23.   }
  24.   return lane;
  25. }
复制代码
它首先找出会通过
  1. getCurrentPriorityLevel
复制代码
方法,根据 Scheduler 中记录的事件优先级,获取任务调度的优先级 schedulerPriority。然后通过
  1. findUpdateLane
复制代码
方法计算得出 lane,作为更新过程中的优先级。
  1. findUpdateLane
复制代码
这个方法中,按照事件的类型,匹配不同级别的 lane,事件类型的优先级划分如下,值越高,代表优先级越高:
  1. // packages/react-reconciler/src/ReactFiberLane.js
  2. export const SyncLanePriority: LanePriority = 15;
  3. export const SyncBatchedLanePriority: LanePriority = 14;
  4. const InputDiscreteHydrationLanePriority: LanePriority = 13;
  5. export const InputDiscreteLanePriority: LanePriority = 12;
  6. const InputContinuousHydrationLanePriority: LanePriority = 11;
  7. export const InputContinuousLanePriority: LanePriority = 10;
  8. const DefaultHydrationLanePriority: LanePriority = 9;
  9. export const DefaultLanePriority: LanePriority = 8;
  10. const TransitionHydrationPriority: LanePriority = 7;
  11. export const TransitionPriority: LanePriority = 6;
  12. const RetryLanePriority: LanePriority = 5;
  13. const SelectiveHydrationLanePriority: LanePriority = 4;
  14. const IdleHydrationLanePriority: LanePriority = 3;
  15. const IdleLanePriority: LanePriority = 2;
  16. const OffscreenLanePriority: LanePriority = 1;
  17. export const NoLanePriority: LanePriority = 0;
复制代码
创建更新对象

eventTime 和 lane 都创建好了之后,就该创建更新了,
  1. createUpdate
复制代码
就是基于上面两个方法所创建的 eventTime 和 lane,去创建一个更新对象:
  1. // packages/react-reconciler/src/ReactUpdateQueue.old.js
  2. export function createUpdate(eventTime: number, lane: Lane): Update<*> {
  3.   const update: Update<*> = {
  4.     eventTime, // 更新要出发的事件
  5.     lane, // 优先级
  6.     tag: UpdateState, // 指定更新的类型,0更新 1替换 2强制更新 3捕获性的更新
  7.     payload: null, // 要更新的内容,例如 setState 接收的第一个参数
  8.     callback: null, // 更新完成后的回调
  9.     next: null, // 指向下一个更新
  10.   };
  11.   return update;
  12. }
复制代码
关联 fiber 的更新队列

创建好了 update 对象之后,紧接着调用
  1. enqueueUpdate
复制代码
方法把update 对象放到 关联的 fiber 的 updateQueue 队列之中:
  1. // packages/react-reconciler/src/ReactUpdateQueue.old.js
  2. export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
  3.   // 获取当前 fiber 的更新队列
  4.   const updateQueue = fiber.updateQueue;
  5.   if (updateQueue === null) {
  6.     // 若 updateQueue 为空,表示 fiber 还未渲染,直接退出
  7.     return;
  8.   }
  9.   const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
  10.   const pending = sharedQueue.pending;
  11.   if (pending === null) {
  12.     // pending 为 null 时表示首次更新,创建循环列表
  13.     update.next = update;
  14.   } else {
  15.     // 将 update 插入到循环列表中
  16.     update.next = pending.next;
  17.     pending.next = update;
  18.   }
  19.   sharedQueue.pending = update;
  20.   // ...
  21. }
复制代码
reconciler 过程

上面的更新任务创建好了并且关联到了 fiber 上,下面就该到了 react render 阶段的核心之一 —— reconciler 阶段。

根据任务类型执行不同更新

reconciler 阶段会协调任务去执行,以
  1. scheduleUpdateOnFiber
复制代码
为入口函数,首先会调用
  1. checkForNestedUpdates
复制代码
方法,检查嵌套的更新数量,若嵌套数量大于 50 层时,被认为是循环更新(无限更新)。此时会抛出异常,避免了例如在类组件 render 函数中调用了 setState 这种死循环的情况。
然后通过
  1. markUpdateLaneFromFiberToRoot
复制代码
方法,向上递归更新 fiber 的 lane,lane 的更新很简单,就是将当前任务 lane 与之前的 lane 进行二进制或运算叠加。
我们看一下其源码:
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.jsexport function scheduleUpdateOnFiber(  fiber: Fiber,  lane: Lane,  eventTime: number,) {  // 检查是否有循环更新  // 避免例如在类组件 render 函数中调用了 setState 这种死循环的情况  checkForNestedUpdates();  // ...  // 自底向上更新 child.fiberLanes  const root = markUpdateLaneFromFiberToRoot(fiber, lane);  // ...  // 标记 root 有更新,将 update 的 lane 插入到root.pendingLanes 中  markRootUpdated(root, lane, eventTime);  if (lane === SyncLane) { // 同步任务,采用同步渲染    if (      (executionContext & LegacyUnbatchedContext) !== NoContext &&      (executionContext & (RenderContext | CommitContext)) === NoContext    ) {      // 如果本次是同步更新,并且当前还未开始渲染      // 表示当前的 js 主线程空闲,并且没有 react 任务在执行      // ...      // 调用 performSyncWorkOnRoot 执行同步更新任务      performSyncWorkOnRoot(root);    } else {      // 如果本次时同步更新,但是有 react 任务正在执行      // 调用 ensureRootIsScheduled 去复用当前正在执行的任务,让其将本次的更新一并执行      ensureRootIsScheduled(root, eventTime);      schedulePendingInteractions(root, lane);      // ...  } else {    // 如果本次更新是异步任务    // ...     // 调用 ensureRootIsScheduled 执行可中断更新    ensureRootIsScheduled(root, eventTime);    schedulePendingInteractions(root, lane);  }  mostRecentlyUpdatedRoot = root;}
复制代码
然后会根据任务类型以及当前线程所处的 react 执行阶段,去判断进行何种类型的更新:

执行同步更新

当任务的类型为同步任务,并且当前的 js 主线程空闲(没有正在执行的 react 任务时),会通过
  1. performSyncWorkOnRoot(root)
复制代码
方法开始执行同步任务。
  1. performSyncWorkOnRoot
复制代码
里面主要做了两件事:

  • renderRootSync 从根节点开始进行同步渲染任务
  • commitRoot 执行 commit 流程
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. function performSyncWorkOnRoot(root) {
  3.   // ...
  4.   exitStatus = renderRootSync(root, lanes);
  5.   // ...
  6.   commitRoot(root);
  7.   // ...
  8. }
复制代码
当任务类型为同步类型,但是 js 主线程非空闲时。会执行
  1. ensureRootIsScheduled
复制代码
方法:
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  3.   // ...
  4.   // 如果有正在执行的任务,
  5.   if (existingCallbackNode !== null) {
  6.     const existingCallbackPriority = root.callbackPriority;
  7.     if (existingCallbackPriority === newCallbackPriority) {
  8.       // 任务优先级没改变,说明可以复用之前的任务一起执行
  9.       return;
  10.     }
  11.     // 任务优先级改变了,说明不能复用。
  12.     // 取消正在执行的任务,重新去调度
  13.     cancelCallback(existingCallbackNode);
  14.   }
  15.   // 进行一个新的调度
  16.   let newCallbackNode;
  17.   if (newCallbackPriority === SyncLanePriority) {
  18.     // 如果是同步任务优先级,执行 performSyncWorkOnRoot
  19.     newCallbackNode = scheduleSyncCallback(
  20.       performSyncWorkOnRoot.bind(null, root),
  21.     );
  22.   } else if (newCallbackPriority === SyncBatchedLanePriority) {
  23.     // 如果是批量同步任务优先级,执行 performSyncWorkOnRoot
  24.     newCallbackNode = scheduleCallback(
  25.       ImmediateSchedulerPriority,
  26.       performSyncWorkOnRoot.bind(null, root),
  27.     );
  28.   } else {
  29.     // ...
  30.     // 如果不是批量同步任务优先级,执行 performConcurrentWorkOnRoot
  31.     newCallbackNode = scheduleCallback(
  32.       schedulerPriorityLevel,
  33.       performConcurrentWorkOnRoot.bind(null, root),
  34.     );
  35.   }
  36.   // ...
  37. }
复制代码
  1. ensureRootIsScheduled
复制代码
方法中,会先看加入了新的任务后根节点任务优先级是否有变更,如果无变更,说明新的任务会被当前的 schedule 一同执行;如果有变更,则创建新的 schedule,然后也是调用
  1. performSyncWorkOnRoot(root)
复制代码
方法开始执行同步任务。

执行可中断更新

当任务的类型不是同步类型时,react 也会执行
  1. ensureRootIsScheduled
复制代码
方法,因为是异步任务,最终会执行
  1. performConcurrentWorkOnRoot
复制代码
方法,去进行可中断的更新,下面会详细讲到。

workLoop


同步

以同步更新为例,
  1. performSyncWorkOnRoot
复制代码
会经过以下流程,
  1. performSyncWorkOnRoot
复制代码
——>
  1. renderRootSync
复制代码
——>
  1. workLoopSync
复制代码
  1. workLoopSync
复制代码
中,只要 workInProgress(workInProgress fiber 树中新创建的 fiber 节点) 不为 null,就会一直循环,执行
  1. performUnitOfWork
复制代码
函数。
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. function workLoopSync() {
  3.   while (workInProgress !== null) {
  4.     performUnitOfWork(workInProgress);
  5.   }
  6. }
复制代码
可中断

可中断模式下,
  1. performConcurrentWorkOnRoot
复制代码
会执行以下过程:
  1. performConcurrentWorkOnRoot
复制代码
——>
  1. renderRootConcurrent
复制代码
——>
  1. workLoopConcurrent
复制代码

相比于
  1. workLoopSync
复制代码
,
  1. workLoopConcurrent
复制代码
在每一次对 workInProgress 执行
  1. performUnitOfWork
复制代码
前,会先判断以下
  1. shouldYield()
复制代码
的值。若为 false 则继续执行,若为 true 则中断执行。
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. function workLoopConcurrent() {
  3.   while (workInProgress !== null && !shouldYield()) {
  4.     performUnitOfWork(workInProgress);
  5.   }
  6. }
复制代码
performUnitOfWork

最终无论是同步执行任务,还是可中断地执行任务,都会进入
  1. performUnitOfWork
复制代码
函数中。
  1. performUnitOfWork
复制代码
中会以 fiber 作为单元,进行协调过程。每次
  1. beginWork
复制代码
执行后都会更新 workIngProgress,从而响应了上面 workLoop 的循环。
直至 fiber 树便利完成后,workInProgress 此时置为 null,执行
  1. completeUnitOfWork
复制代码
函数。
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. function performUnitOfWork(unitOfWork: Fiber): void {
  3.   // ...
  4.   const current = unitOfWork.alternate;
  5.   // ...
  6.   let next;
  7.   if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
  8.     // ...
  9.     next = beginWork(current, unitOfWork, subtreeRenderLanes);
  10.   } else {
  11.     next = beginWork(current, unitOfWork, subtreeRenderLanes);
  12.   }
  13.   // ...
  14.   if (next === null) {
  15.     completeUnitOfWork(unitOfWork);
  16.   } else {
  17.     workInProgress = next;
  18.   }
  19.   ReactCurrentOwner.current = null;
  20. }
复制代码
beginWork
  1. beginWork
复制代码
是根据当前执行环境,封装调用了
  1. originalBeginWork
复制代码
函数:
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. let beginWork;
  3. if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
  4.   beginWork = (current, unitOfWork, lanes) => {
  5.     // ...
  6.     try {
  7.       return originalBeginWork(current, unitOfWork, lanes);
  8.     } catch (originalError) {
  9.       // ...
  10.     }
  11.   };
  12. } else {
  13.   beginWork = originalBeginWork;
  14. }
复制代码
  1. originalBeginWork
复制代码
中,会根据 workInProgress 的 tag 属性,执行不同类型的 react 元素的更新函数。但是他们都大同小异,不论是 tag 是何种类型,更新函数最终都会去调用
  1. reconcileChildren
复制代码
函数。
  1. // packages/react-reconciler/src/ReactFiberBeginWork.old.js
  2. function beginWork(
  3.   current: Fiber | null,  workInProgress: Fiber,  renderLanes: Lanes,
  4. ): Fiber | null {
  5.   const updateLanes = workInProgress.lanes;
  6.   workInProgress.lanes = NoLanes;
  7.   // 针对 workInProgress 的tag,执行相应的更新
  8.   switch (workInProgress.tag) {
  9.     // ...
  10.     case HostRoot:
  11.       return updateHostRoot(current, workInProgress, renderLanes);
  12.     case HostComponent:
  13.       return updateHostComponent(current, workInProgress, renderLanes);
  14.     // ...
  15.   }
  16.   // ...
  17. }
复制代码
  1. updateHostRoot
复制代码
为例,根据根 fiber 是否存在,去执行 mountChildFibers 或者 reconcileChildren:
  1. // packages/react-reconciler/src/ReactFiberBeginWork.old.js
  2. function updateHostRoot(current, workInProgress, renderLanes) {
  3.   // ...
  4.   const root: FiberRoot = workInProgress.stateNode;
  5.   if (root.hydrate && enterHydrationState(workInProgress)) {
  6.     // 若根 fiber 不存在,说明是首次渲染,调用 mountChildFibers
  7.     // ...
  8.     const child = mountChildFibers(
  9.       workInProgress,
  10.       null,
  11.       nextChildren,
  12.       renderLanes,
  13.     );
  14.     workInProgress.child = child;
  15.   } else {
  16.     // 若根 fiber 存在,调用 reconcileChildren
  17.     reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  18.     resetHydrationState();
  19.   }
  20.   return workInProgress.child;
  21. }
复制代码
  1. reconcileChildren
复制代码
做的事情就是 react 的另一核心之一 —— diff 过程,在下一篇文章中会详细讲。

completeUnitOfWork

当 workInProgress 为 null 时,也就是当前任务的 fiber 树遍历完之后,就进入到了
  1. completeUnitOfWork
复制代码
函数。
经过了
  1. beginWork
复制代码
操作,workInProgress 节点已经被打上了flags 副作用标签。
  1. completeUnitOfWork
复制代码
方法中主要是逐层收集 effects
链,最终收集到 root 上,供接下来的commit阶段使用。
  1. completeUnitOfWork
复制代码
结束后,render 阶段便结束了,后面就到了 commit 阶段。
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. function completeUnitOfWork(unitOfWork: Fiber): void {
  3.   let completedWork = unitOfWork;
  4.   do {
  5.     // ...
  6.     // 对节点进行completeWork,生成DOM,更新props,绑定事件
  7.     next = completeWork(current, completedWork, subtreeRenderLanes);
  8.     if (
  9.       returnFiber !== null &&
  10.       (returnFiber.flags & Incomplete) === NoFlags
  11.     ) {
  12.       // 将当前节点的 effectList 并入到父节点的 effectList
  13.       if (returnFiber.firstEffect === null) {
  14.         returnFiber.firstEffect = completedWork.firstEffect;
  15.       }
  16.       if (completedWork.lastEffect !== null) {
  17.         if (returnFiber.lastEffect !== null) {
  18.           returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
  19.         }
  20.         returnFiber.lastEffect = completedWork.lastEffect;
  21.       }
  22.       // 将自身添加到 effectList 链,添加时跳过 NoWork 和 PerformedWork的 flags,因为真正的 commit 时用不到
  23.       const flags = completedWork.flags;
  24.       if (flags > PerformedWork) {
  25.         if (returnFiber.lastEffect !== null) {
  26.           returnFiber.lastEffect.nextEffect = completedWork;
  27.         } else {
  28.           returnFiber.firstEffect = completedWork;
  29.         }
  30.         returnFiber.lastEffect = completedWork;
  31.       }
  32.     }
  33.   } while (completedWork !== null);
  34.   // ...
  35. }
复制代码
实现帧空闲调度任务

刚刚上面说到了在执行可中断的更新时,浏览器会在每一帧空闲时刻去执行 react 更新任务,那么空闲时刻去执行是如何实现的呢?我们很容易联想到一个 api —— requestIdleCallback。但由于 requestIdleCallback 的兼容性问题以及 react 对应部分高优先级任务可能牺牲部分帧的需要,react 通过自己实现了类似的功能代替了 requestIdleCallback。
我们上面讲到执行可中断更新时,
  1. performConcurrentWorkOnRoot
复制代码
函数时通过
  1. scheduleCallback
复制代码
包裹起来的:
  1. scheduleCallback(
  2.   schedulerPriorityLevel,
  3.   performConcurrentWorkOnRoot.bind(null, root),
  4. );
复制代码
  1. scheduleCallback
复制代码
函数是引用了
  1. packages/scheduler/src/Scheduler.js
复制代码
路径下的
  1. unstable_scheduleCallback
复制代码
函数,我们来看一下这个函数,它会去按计划插入调度任务:
  1. // packages/scheduler/src/Scheduler.js
  2. function unstable_scheduleCallback(priorityLevel, callback, options) {
  3.   // ...
  4.   if (startTime > currentTime) {
  5.     // 当前任务已超时,插入超时队列
  6.     // ...
  7.   } else {
  8.     // 任务未超时,插入调度任务队列
  9.     newTask.sortIndex = expirationTime;
  10.     push(taskQueue, newTask);
  11.     // 符合更新调度执行的标志
  12.     if (!isHostCallbackScheduled && !isPerformingWork) {
  13.       isHostCallbackScheduled = true;
  14.       // requestHostCallback 调度任务
  15.       requestHostCallback(flushWork);
  16.     }
  17.   }
  18.   return newTask;
  19. }
复制代码
将任务插入了调度队列之后,会通过
  1. requestHostCallback
复制代码
函数去调度任务。
react 通过
  1. new MessageChannel()
复制代码
创建了消息通道,当发现 js 线程空闲时,通过 postMessage 通知 scheduler 开始调度。然后 react 接收到调度开始的通知时,就通过
  1. performWorkUntilDeadline
复制代码
函数去更新当前帧的结束时间,以及执行任务。从而实现了帧空闲时间的任务调度。
  1. // packages/scheduler/src/forks/SchedulerHostConfig.default.js
  2. // 获取当前设备每帧的时长
  3. forceFrameRate = function(fps) {
  4.   // ...
  5.   if (fps > 0) {
  6.     yieldInterval = Math.floor(1000 / fps);
  7.   } else {
  8.     yieldInterval = 5;
  9.   }
  10. };
  11. // 帧结束前执行任务
  12. const performWorkUntilDeadline = () => {
  13.   if (scheduledHostCallback !== null) {
  14.     const currentTime = getCurrentTime();
  15.     // 更新当前帧的结束时间
  16.     deadline = currentTime + yieldInterval;
  17.     const hasTimeRemaining = true;
  18.     try {
  19.       const hasMoreWork = scheduledHostCallback(
  20.         hasTimeRemaining,
  21.         currentTime,
  22.       );
  23.       // 如果还有调度任务就执行
  24.       if (!hasMoreWork) {
  25.         isMessageLoopRunning = false;
  26.         scheduledHostCallback = null;
  27.       } else {
  28.         // 没有调度任务就通过 postMessage 通知结束
  29.         port.postMessage(null);
  30.       }
  31.     } catch (error) {
  32.       // ..
  33.       throw error;
  34.     }
  35.   } else {
  36.     isMessageLoopRunning = false;
  37.   }
  38.   needsPaint = false;
  39. };
  40. // 通过 MessageChannel 创建消息通道,实现任务调度通知
  41. const channel = new MessageChannel();
  42. const port = channel.port2;
  43. channel.port1.onmessage = performWorkUntilDeadline;
  44. // 通过 postMessage,通知 scheduler 已经开始了帧调度
  45. requestHostCallback = function(callback) {
  46.   scheduledHostCallback = callback;
  47.   if (!isMessageLoopRunning) {
  48.     isMessageLoopRunning = true;
  49.     port.postMessage(null);
  50.   }
  51. };
复制代码
任务中断

前面说到可中断模式下的 workLoop,每次遍历执行 performUnitOfWork 前会先判断
  1. shouYield
复制代码
的值
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. function workLoopConcurrent() {
  3.   while (workInProgress !== null && !shouldYield()) {
  4.     performUnitOfWork(workInProgress);
  5.   }
  6. }
复制代码
我们看一下
  1. shouYield
复制代码
的值是如何获取的:
  1. // packages\scheduler\src\SchedulerPostTask.js
  2. export function unstable_shouldYield() {
  3.   return getCurrentTime() >= deadline;
  4. }
复制代码
  1. getCurrentTime
复制代码
获取的是当前的时间戳,deadline 上面讲到了是浏览器每一帧结束的时间戳。也就是说 concurrent 模式下,react 会将这些非同步任务放到浏览器每一帧空闲时间段去执行,若每一帧结束未执行完,则中断当前任务,待到浏览器下一帧的空闲再继续执行。

总结

总结一下 react render 阶段的设计思想:
当发生渲染或者更新操作时,react 去创建一系列的任务,任务带有优先级,然后构建 workInProgress fiber 树链表。
遍历任务链表去执行任务。每一帧帧先执行浏览器的渲染等任务,如果当前帧还有空闲时间,则执行任务,直到当前帧的时间用完。如果当前帧已经没有空闲时间,就等到下一帧的空闲时间再去执行。如果当前帧没有空闲时间但是当前任务链表有任务到期了或者有立即执行任务,那么必须执行的时候就以丢失几帧的代价,执行这些任务。执行完的任务都会被从链表中删除。
到此这篇关于React render核心阶段深入探究穿插scheduler与reconciler的文章就介绍到这了,更多相关React render内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

来源:https://www.jb51.net/article/266747.htm
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具