您现在的位置是:网站首页> 编程资料编程资料

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

2023-12-09 274人已围观

简介 React render核心阶段深入探究穿插scheduler与reconciler_React_

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

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

触发更新

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

ReactDOM.render

ReactDOM.render 作为 react 应用程序的入口函数,在页面首次渲染时便会触发,页面 dom 的首次创建,也属于触发 react 更新的一种情况。

首先调用 legacyRenderSubtreeIntoContainer 函数,校验根节点 root 是否存在,若不存在,调用 legacyCreateRootFromDOMContainer 创建根节点 root、rootFiber 和 fiberRoot 并绑定它们之间的引用关系,然后调用 updateContainer 去非批量执行后面的更新流程;若存在,直接调用 updateContainer 去批量执行后面的更新流程:

// packages/react-dom/src/client/ReactDOMLegacy.js function legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component, children: ReactNodeList, container: Container, forceHydrate: boolean, callback: ?Function, ) { // ... let root: RootType = (container._reactRootContainer: any); let fiberRoot; if (!root) { // 首次渲染时根节点不存在 // 通过 legacyCreateRootFromDOMContainer 创建根节点、fiberRoot 和 rootFiber root = container._reactRootContainer = legacyCreateRootFromDOMContainer( container, forceHydrate, ); fiberRoot = root._internalRoot; if (typeof callback === 'function') { const originalCallback = callback; callback = function() { const instance = getPublicRootInstance(fiberRoot); originalCallback.call(instance); }; } // 非批量执行更新流程 unbatchedUpdates(() => { updateContainer(children, fiberRoot, parentComponent, callback); }); } else { fiberRoot = root._internalRoot; if (typeof callback === 'function') { const originalCallback = callback; callback = function() { const instance = getPublicRootInstance(fiberRoot); originalCallback.call(instance); }; } // 批量执行更新流程 updateContainer(children, fiberRoot, parentComponent, callback); } return getPublicRootInstance(fiberRoot); }

updateContainer 函数中,主要做了以下几件事情:

  • requestEventTime:获取更新触发的时间
  • requestUpdateLane:获取当前任务优先级
  • createUpdate:创建更新
  • enqueueUpdate:将任务推进更新队列
  • scheduleUpdateOnFiber:调度更新

关于这几个函数稍后会详细讲到

// packages/react-dom/src/client/ReactDOMLegacy.js export function updateContainer( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component, callback: ?Function, ): Lane { // ... const current = container.current; const eventTime = requestEventTime(); // 获取更新触发的时间 // ... const lane = requestUpdateLane(current); // 获取任务优先级 if (enableSchedulingProfiler) { markRenderScheduled(lane); } const context = getContextForSubtree(parentComponent); if (container.context === null) { container.context = context; } else { container.pendingContext = context; } // ... const update = createUpdate(eventTime, lane); // 创建更新任务 update.payload = {element}; callback = callback === undefined ? null : callback; if (callback !== null) { // ... update.callback = callback; } enqueueUpdate(current, update); // 将任务推入更新队列 scheduleUpdateOnFiber(current, lane, eventTime); // schedule 进行调度 return lane; }

setState

setState 时类组件中我们最常用的修改状态的方法,状态修改会触发更新流程。

class 组件在原型链上定义了 setState 方法,其调用了触发器 updater 上的 enqueueSetState 方法:

// packages/react/src/ReactBaseClasses.js Component.prototype.setState = function(partialState, callback) { invariant( typeof partialState === 'object' || typeof partialState === 'function' || partialState == null, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.', ); this.updater.enqueueSetState(this, partialState, callback, 'setState'); };

然后我们再来看以下 updater 上定义的 enqueueSetState 方法,一看到这我们就了然了,和 updateContainer 方法中做的事情几乎一模一样,都是触发后续的更新调度。

// packages/react-reconciler/src/ReactFiberClassComponent.old.js const classComponentUpdater = { isMounted, enqueueSetState(inst, payload, callback) { const fiber = getInstance(inst); const eventTime = requestEventTime(); // 获取更新触发的时间 const lane = requestUpdateLane(fiber); // 获取任务优先级 const update = createUpdate(eventTime, lane); // 创建更新任务 update.payload = payload; if (callback !== undefined && callback !== null) { if (__DEV__) { warnOnInvalidCallback(callback, 'setState'); } update.callback = callback; } enqueueUpdate(fiber, update); // 将任务推入更新队列 scheduleUpdateOnFiber(fiber, lane, eventTime); // schedule 进行调度 // ... if (enableSchedulingProfiler) { markStateUpdateScheduled(fiber, lane); } }, // ... };

forceUpdate

forceUpdate 的流程与 setState 几乎一模一样:

同样其调用了触发器 updater 上的 enqueueForceUpdate 方法,enqueueForceUpdate 方法也同样是触发了一系列的更新流程:相关参考视频讲解:传送门

reconciler/src/ReactFiberClassComponent.old.js const classComponentUpdater = { isMounted, // ... enqueueForceUpdate(inst, callback) { const fiber = getInstance(inst); const eventTime = requestEventTime(); // 获取更新触发的时间 const lane = requestUpdateLane(fiber); // 获取任务优先级 const update = createUpdate(eventTime, lane); // 创建更新 update.tag = ForceUpdate; if (callback !== undefined && callback !== null) { if (__DEV__) { warnOnInvalidCallback(callback, 'forceUpdate'); } update.callback = callback; } enqueueUpdate(fiber, update); // 将任务推进更新队列 scheduleUpdateOnFiber(fiber, lane, eventTime); // 触发更新调度 // ... if (enableSchedulingProfiler) { markForceUpdateScheduled(fiber, lane); } }, };

创建更新任务

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

获取更新触发时间

前面的文章中我们讲到过,react 执行更新过程中,会将更新任务拆解,每一帧优先执行高优先级的任务,从而保证用户体验的流畅。那么即使对于同样优先级的任务,在任务多的情况下该优先执行哪一些呢?

react 通过 requestEventTime 方法去创建一个 currentEventTime,用于标识更新任务触发的时间,对于相同时间的任务,会批量去执行。同样优先级的任务,currentEventTime 值越小,就会越早执行。

我们看一下 requestEventTime 方法的实现:

// packages/react-reconciler/src/ReactFiberWorkLoop.old.js export function requestEventTime() { if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { // 在 react 执行过程中,直接返回当前时间 return now(); } // 如果不在 react 执行过程中 if (currentEventTime !== NoTimestamp) { // 正在执行浏览器事件,返回上次的 currentEventTime return currentEventTime; } // react 中断后首次更新,计算新的 currentEventTime currentEventTime = now(); return currentEventTime; }

在这个方法中,(executionContext & (RenderContext | CommitContext) 做了二进制运算,RenderContext 代表着 react 正在计算更新,CommitContext 代表着 react 正在提交更新。所以这句话是判断当前 react 是否处在计算或者提交更新的阶段,如果是则直接返回 now()

// packages/react-reconciler/src/ReactFiberWorkLoop.old.js export const NoContext = /* */ 0b0000000; const BatchedContext = /* */ 0b0000001; const EventContext = /* */ 0b0000010; const DiscreteEventContext = /* */ 0b0000100; const LegacyUnbatchedContext = /* */ 0b0001000; const RenderContext = /* */ 0b0010000; const CommitContext = /* */ 0b0100000; export const RetryAfterError = /* */ 0b1000000; let executionContext: ExecutionContext = NoContext;

再来看一下 now 的代码,这里的意思时,当前后的更新任务时间差小于 10ms 时,直接采用上次的 Scheduler_now,这样可以抹平 10ms 内更新任务的时间差, 有利于批量更新:

// packages/react-reconciler/src/SchedulerWithReactIntegration.old.js export const now = initialTimeMs < 10000 ? Scheduler_now : () => Scheduler_now() - initialTimeMs; 

综上所述,requestEvent 做的事情如下:

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

-六神源码网