您现在的位置是:网站首页> 编程资料编程资料
React render核心阶段深入探究穿插scheduler与reconciler_React_
2023-01-18
688人已围观
简介 React render核心阶段深入探究穿插scheduler与reconciler_React_
本章将讲解 react 的核心阶段之一 —— render阶段,我们将探究以下部分内容的源码:
- 更新任务的触发
- 更新任务的创建
- reconciler 过程同步和异步遍历及执行任务
- scheduler 是如何实现帧空闲时间调度任务以及中断任务的
触发更新
触发更新的方式主要有以下几种:ReactDOM.render、setState、forUpdate 以及 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); } }, };创建更新任务
可以发现,上述的三种触发更新的动作,最后殊途同归,都会走上述流程图中从 requestEventTime 到 scheduleUpdateOnFiber 这一流程,去创建更新任务,先我们详细看下更新任务是如何创建的。
获取更新触发时间
前面的文章中我们讲到过,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 上次中断之后的首次更新,那么给 currentEventTime 赋一个新的值
划分更新任务优先级
说完了相同优先级任务的触发时间,那么任务的优先级又是如何划分的呢?这里就要提到 requestUpdateLane,我们来看一下其源码:
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js export function requestUpdateLane(fiber: Fiber): Lane { // ... // 根据记录下的事件的优先级,获取任务调度的优先级 const schedulerPriority = getCurrentPriorityLevel(); // ... let lane; if ( (executionContext & DiscreteEventContext) !== NoContext && schedulerPriority === UserBlockingSchedulerPriority ) { // 如果是用户阻塞级别的事件,则通过InputDiscreteLanePriority 计算更新的优先级 lane lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes); } else { // 否则依据事件的优先级计算 schedulerLanePriority const schedulerLanePriority = schedulerPriorityToLanePriority( schedulerPriority, ); if (decoupleUpdatePriorityFromScheduler) { const currentUpdateLanePriority = getCurrentUpdateLanePriority(); // 根据计算得到的 schedulerLanePriority,计算更新的优先级 lane lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes); } return lane; }它首先找出会通过 getCurrentPriorityLevel 方法,根据 Scheduler 中记录的事件优先级,获取任务调度的优先级 schedulerPriority。然后通过 findUpdateLane 方法计算得出 lane,作为更新过程中的优先级。
findUpdateLane 这个方法中,按照事件的类型,匹配不同级别的 lane,事件类型的优先级划分如下,值越高,代表优先级越高:
// packages/react-reconciler/src/ReactFiberLane.js export const SyncLanePriority: LanePriority = 15; export const SyncBatchedLanePriority: LanePriority = 14; const InputDiscreteHydrationLanePriority: LanePriority = 13; export const InputDiscreteLanePriority: LanePriority = 12; const InputContinuousHydrationLanePriority: LanePriority = 11; export const InputContinuousLanePriority: LanePriority = 10; const DefaultHydrationLanePriority: LanePriority = 9; export const DefaultLanePriority: LanePriority = 8; const TransitionHydrationPriority: LanePriority = 7; export const TransitionPriority: LanePriority = 6; const RetryLanePriority: LanePriority = 5; const SelectiveHydrationLanePriority: LanePriority = 4; const IdleHydrationLanePriority: LanePriority = 3; const IdleLanePriority: LanePriority = 2; const OffscreenLanePriority: LanePriority = 1; export const NoLanePriority: LanePriority = 0;
创建更新对象
eventTime 和 lane 都创建好了之后,就该创建更新了,createUpdate 就是基于上面两个方法所创建的 eventTime 和 lane,去创建一个更新对象:
// packages/react-reconciler/src/ReactUpdateQueue.old.js export function createUpdate(eventTime: number, lane: Lane): Update<*> { const update: Update<*> = { eventTime, // 更新要出发的事件 lane, // 优先级 tag: UpdateState, // 指定更新的类型,0更新 1替换 2强制更新 3捕获性的更新 payload: null, // 要更新的内容,例如 setState 接收的第一个参数 callback: null, // 更新完成后的回调 next: null, // 指向下一个更新 }; return update; }关联 fiber 的更新队列
创建好了 update 对象之后,紧接着调用 enqueueUpdate 方法把update 对象放到 关联的 fiber 的 updateQueue 队列之中:
// packages/react-reconciler/src/ReactUpdateQueue.old.js export function enqueueUpdate(fiber: Fiber, update: Update ) { // 获取当前 fiber 的更新队列 const updateQueue = fiber.updateQueue; if (updateQueue === null) { // 若 updateQueue 为空,表示 fiber 还未渲染,直接退出 return; } const sharedQueue: SharedQueue = (updateQueue: any).shared; const pending = sharedQueue.pending; if (pending === null) { // pending 为 null 时表示首次更新,创建循环列表 update.next = update; } else { // 将 update 插入到循环列表中 update.next = pending.next; pending.next = update; } sharedQueue.pending = update; // ... }
reconciler 过程
上面的更新任务创建好了并且关联到了 fiber 上,下面就该到了 react render 阶段的核心之一 —— reconciler 阶段。
根据任务类型执行不同更新
reconciler 阶段会协调任务去执行,以 scheduleUpdateOnFiber 为入口函数,首先会调用 checkForNestedUpdates 方法,检查嵌套的更新数量,若嵌套数量大于 50 层时,被认为是循环更新(无限更新)。此时会抛出异常,避免了例如在类组件 render 函数中调用了 setState 这种死循环的情况。
然后通过 markUpdateLaneFromFiberToRoot 方法,向上递归更新 fiber 的 lane,lane 的更新很简单,就是将当前任务 lane 与之前的 lane 进行二进制或运算叠加。
我们看一下其源码:
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js export 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 任务时),会通过 performSyncWorkOnRoot(root) 方法开始执行同步任务。
performSyncWorkOnRoot 里面主要做了两件事:
- renderRootSync 从根节点开始进行同步渲染任务
- commitRoot 执行 commit 流程
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js function performSyncWorkOnRoot(root) { // ... exitStatus = renderRootSync(root, lanes); // ... commitRoot(root); // ... }当任务类型为同步类型,但是 js 主线程非空闲时。会执行
ens
相关内容
- React运行机制超详细讲解_React_
- 使用JavaScript实现按钮的涟漪效果实例代码_javascript技巧_
- React深入分析useEffect源码_React_
- React.memo 和 useMemo 的使用问题小结_React_
- Vue+ ArcGIS JavaScript APi详解_vue.js_
- node+vue前后端分离实现登录时使用图片验证码功能_vue.js_
- React Fiber源码深入分析_React_
- vue3中使用Apache ECharts的详细方法_vue.js_
- React深入浅出分析Hooks源码_React_
- node path的使用详解_node.js_
