react17.x源码解析(2)——fiber树的构建与更新
2022/2/18 20:20:19
本文主要是介绍react17.x源码解析(2)——fiber树的构建与更新,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
下面我们结合源码,来看一下实际工作过程中fiber树的构建与更新过程。
mount过程
react首次mount开始执行时,以ReactDOM.render为入口函数,会经过如下一系列的函数调用:
ReactDOM.render——> legacyRenderSubtreeIntoContainer——>legacyCreateRootFromDOMContainer——>createLegacyRoot——>ReactDOMBlockingRoot——>ReactDOMRoot——>createRootImpl——>createContainer——>createFiberRoot——>createHostFiber——>createFiber
在createFiber函数中,调用FiberNode构造函数,创建了rootFiber,他是react应用的根fiber:
// packages/react-reconciler/src/ReactFiber.old.js const createFiber = function( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ): Fiber { return new FiberNode(tag, pendingProps, key, mode); };
在createFiberRoot函数中,调用FiberRootNode构造函数,创建了fiberRoot,他指向真实根dom节点。
// packages/react-reconciler/src/ReactFiberRoot.old.js export function createFiberRoot( containerInfo: any, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): FiberRoot { const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any); if (enableSuspenseCallback) { root.hydrationCallbacks = hydrationCallbacks; } const uninitializedFiber = createHostRootFiber(tag); root.current = uninitializedFiber; uninitializedFiber.stateNode = root; initializeUpdateQueue(uninitializedFiber); return root; }
另外 createFiberRoot函数中,还让rootFiber的stateNode字段指向了fiberRoot,fiberRoot的current字段指向了rootFiber。从而一颗最原始的fiber树根节点就创建完成了:
上面的rootFiber和fiberRoot创建完成后,react就会根据jsx的内容去创建详细的dom树了,例如有如下的jsx:
react对于fiber结构的创建和 更新,都是采用深度优先遍历,从rootFiber(此处对应id为root的节点)开始,首先创建child a1,然后发现a1有子节点b1,继续对b1进行遍历,b1有子节点c1,再去创建c1的子节点d1、d2、d3,直至发现d1、d2、d3都没有子节点来了,再回去创建c2.
上面的过程中,每个节点开始创建时,执行beginWork流程,直至该节点的所有子孙节点都创建(更新)完成后,执行completeWork流程,流程的图示如下:
update过程
update时,react会根据新的jsx内容创建新的workInProgress fiber,还是通过深度优先遍历,对发生改变的fiber打上不同的flags副作用标签,并通过firstEffect、nextEffect等字段形成Effect List链表。
例如上面的jsx结构,发生了如下的更新:
react会根据新的jsx解析后的内容调用createWorkInProgress函数创建workInProgress fiber,对其标记副作用:
// packages/react-reconciler/src/ReactFiber.old.js export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { let workInProgress = current.alternate; if (workInProgress === null) { // 区分 mount 还是 update workInProgress = createFiber( current.tag, pendingProps, current.key, current.mode, ); workInProgress.elementType = current.elementType; workInProgress.type = current.type; workInProgress.stateNode = current.stateNode; if (__DEV__) { workInProgress._debugID = current._debugID; workInProgress._debugSource = current._debugSource; workInProgress._debugOwner = current._debugOwner; workInProgress._debugHookTypes = current._debugHookTypes; } workInProgress.alternate = current; current.alternate = workInProgress; } else { workInProgress.pendingProps = pendingProps; workInProgress.type = current.type; workInProgress.subtreeFlags = NoFlags; workInProgress.deletions = null; if (enableProfilerTimer) { workInProgress.actualDuration = 0; workInProgress.actualStartTime = -1; } } // 重置所有的副作用 workInProgress.flags = current.flags & StaticMask; workInProgress.childLanes = current.childLanes; workInProgress.lanes = current.lanes; workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; workInProgress.memoizedState = current.memoizedState; workInProgress.updateQueue = current.updateQueue; // 克隆依赖 const currentDependencies = current.dependencies; workInProgress.dependencies = currentDependencies === null ? null : { lanes: currentDependencies.lanes, firstContext: currentDependencies.firstContext, }; workInProgress.sibling = current.sibling; workInProgress.index = current.index; workInProgress.ref = current.ref; if (enableProfilerTimer) { workInProgress.selfBaseDuration = current.selfBaseDuration; workInProgress.treeBaseDuration = current.treeBaseDuration; } if (__DEV__) { workInProgress._debugNeedsRemount = current._debugNeedsRemount; switch (workInProgress.tag) { case IndeterminateComponent: case FunctionComponent: case SimpleMemoComponent: workInProgress.type = resolveFunctionForHotReloading(current.type); break; case ClassComponent: workInProgress.type = resolveClassForHotReloading(current.type); break; case ForwardRef: workInProgress.type = resolveForwardRefForHotReloading(current.type); break; default: break; } } return workInProgress; }
最终生成的 workInProgress fiber图示如下:
然后如上面所说,current fiber和workInProgress fiber中对应的alternate会相互指向,然后workInProgress fiber完全创建完成后,fiberRoot的current字段的指向会从current fiber中的rootFiber改为workInProgress fiber 中的rootFiber:
然后如上面所说,current fiber和workInProgress fiber中对应的alternate会相互指向,然后workInProgress fiber 完全创建完成后,fiberRoot的current字段的指向会从current fiber中的rootFiber改为workInProgress fiber中的rootFiber:
下面我们将探究以下部分内容的源码:
- 更新任务的触发
- 更新任务的插件
- reconciler过程同步和异步遍历及执行任务
- scheduler是如何实现帧空闲时间调度任务以及中断任务的
触发更新
触发更新的方式主要有以下几种:ReactDOM.render、setState、forUpdate以及hooks中的useState等,关于hooks的我们后面再详细讲解,这里先关注前三种情况。
ReactDOM.render
ReactDOM.render作为react应用程序的入口函数,在页面首次渲染时便会触发,页面dom的首次创建,也属于触发react更新的一种情况。其整体流程如下:
首先调用legacyRenderSubtreeIntoContainer函数,校验根节点root是否存在,若不存在,调用legacyCreateRootFromDOMContainer创建根节点root、rootFiber和fiberRoot并绑定他们之间的引用关系,然后调用updateContaioner去批量执行后面的更新流程;若存在,直接调用updateContainer去批量执行后面的更新流程:
// packages/react-dom/src/client/ReactDOMLegacy.js function legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component<any, any>, 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:调度更新
关于这几个函数稍后会详细降到:
export function updateContainer( element:ReactNodeList, container:OpaqueRoot, parentComponent:?React$Component<any,any>, 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方法:
然后我们再来看以下updater上定义的enqueueSetState方法,一看到这我们就了然了,和updateContainer方法中做的事情几乎一模一样,都是触发后续的更新调度。
forceUpdate
forceUpdate的流程与setState几乎一模一样:
同样其调用了触发器updater上的enqueueForceUpdate方法,enqueueForceUpdate方法也同样是触发了一系列的更新流程:
创建更新任务
可以发现,上述的三种触发更新的动作,最后殊途同归,都会走上上述流程图中从requestEventTime到scheduleUpdateOnFiber这一流程,去创建更新任务,我们先详细看一下更新任务是如何创建的。
获取更新触发时间
前面讲到过,react执行更新过程中,会将更新任务拆解,每一帧优先执行高优先级的任务,从而保证用户体验的流畅。那么即使对于同样优先级的任务,在任务多的情况下该优先执行哪一些呢?
react通过requestEventTime方法去创建一个currentEventTime,用于标识更新任务触发的时间,对于相同时间的任务,会批量去执行。同样优先级的任务,currentEventTime值越小,就会越早执行。
我们看一下requestEventTime方法的实现:
在这个方法中,(executionContext & (RenderContext | CommitContext))做了二进制运算,RenderContext代表着react正在计算更新,CommitContext代表着react正在提交更新。所以这句话是判断当前react是否处在计算或者提交更新的阶段,如果是则直接返回now()。
再来看一下now的代码,这里的意思是,当前后的更新任务时间差小于10ms时,直接采用上次的Scheduler_now,这样可以抹平10ms内更新任务的时间差,有利于批量更新:
// packages/react-reconciler/src/SchedulerWithReactIntegration.old.js export const now = initailTimeMs < 10000 ? Scheuler_now : ()=>Scheduler_now() - initialTimeMs;
综上所述,requestEvent做的事情如下:
- 在react的render和commit阶段我们直接获取更新任务的触发事件,并抹平相差10ms以内的更新任务以便批量执行。
- 当currentEventTime不等于NoTimestamp时,则判断其正在执行浏览器事件,react想要同样优先级的更新任务保持相同的时间,所以直接返回上次的currentEventTime
- 如果是react上次中断之后的首次更新,那么给currentEventTime赋一个新的值
划分更新任务优先级
说完了相同优先级任务的触发时间,那么任务的优先级又是如何划分的呢?这里就要提到requestUpdateLane,我们来看一下源码:
它首先找出会通过getCurrentPriorityLevel方法,根据Scheduler中记录的事件优先级,获取任务调度的优先级schedulerPriority。然后通过findUpdateLane方法计算得出lane,作为更新过程中的优先级。
findUpdateLane这个方法中,按照事件的类型,匹配不同级别的lane,事件类型的优先级划分如下,值越高,代表的优先级越高:
创建更新对象
eventTime和lane都创建好了之后,就该创建更新了,createUpdate就是基于上面两个方法所创建的eventTime和lane,去创建一个更新对象:
关联fiber的更新对列
创建好了update对象之后,紧接着调用enqueueUpdate方法把update对象放到关联的fiber的updateQueue对列之中:
// packages/react-reconciler/src/ReactUpdateQueue.old.js export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) { // 获取当前 fiber 的更新队列 const updateQueue = fiber.updateQueue; if (updateQueue === null) { // 若 updateQueue 为空,表示 fiber 还未渲染,直接退出 return; } const sharedQueue: SharedQueue<State> = (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阶段会协调任务去执行,以scheduleUpdateOnFiber为入口函数,首先会调用checkForNestedUpdates方法,检查嵌套的更新数量,若嵌套数量大于50层时,被认为是循环更新(无限更新)。此时会抛出异常,避免了例如在类组件render函数中调用了setState这种死循环的情况。
然后通过markUpdateLaneFromFiberToRoot方法,向上递归更新fiber的lane,lane的更新很简单,就是将当前任务lane与之前lane进行二进制或运算叠加。
然后会根据任务类型以及当前线程所处的react执行阶段,去判断进行何种类型的更新:
执行同步更新
当任务的类型为同步任务,并且当前的js主线程空闲(没有正在执行的react任务时),会通过performSyncWorkOnRoot(root)方法开始执行同步任务。
performSynWorkOnRoot(root)方法开始执行同步任务。
performSyncWorkOnRoot里面主要做了两件事:
- renderRootSync从根节点开始进行同步渲染任务
- commitRoot执行commit流程
当任务类型为同步类型,但是js主线程非空闲时。会执行ensureRootIsScheduled方法:
ensureRootIsScheduled方法中,会先看到加入了新的任务后根节点任务优先级是否有变更,如果无变更,说明新的任务会被当前的schedule一同执行;如果有变更,则创建新的schedule,然后也是调用performSyncWorkOnRoot(root)方法开始执行同步任务。
执行可中断更新
当任务的类型不是同类型时,react也会执行ensureRootIsScheduled方法,因为是异步任务,最终会执行performConcurrentWorkOnRoot方法,去进行可中断的更新,下面会详细讲到。
workLoop
同步
以同步更新为例,performSyncWorkOnRoot 会经过以下流程,performSyncWorkOnRoot——>renderRootSync——>workLoopSync。
workLoopSync中,只要workInProgress(workInProgress fiber树中新创建的fiber节点) 不为null,就会一直循环,执行performUnitOfwork函数。
可中断
可中断模式下,performConcurrentWorkOnRoot会执行以下过程:performConcurrentWorkOnRoot——>renderRootConcurrent——>workLoopConcurrent。
相比较于workLoopSync,workLoopConcurrent在每一次对workInProgress执行performUnitOfWork前,会先判断以下shouldYield()的值。若为false则继续执行,若为true则中断执行。
performUnitOfWork
最终无论是同步执行任务,还是可中断地执行任务,都会进入performUnitOfWork函数中。
performUnitOfWork中会以fiber作为单元,进行协调过程。每次beginWork执行后都会更新workIngProgress,从而响应了上面workLoop的循环。
直至fiber树便利完成后,workInProgress此时值为null,执行completeUnitOfWork函数。
beginWork
beginWork是根据当前执行环境,封装调用了originalBeginWork函数:
originalBeginWork中,会根据workInProgress的tag属性,执行不同类型的react元素的更新函数。但是他们都大同小异,不论是tag是何种类型,更新函数最终都会去调用reconcileChildren函数。
以updateHostRoot为例,根据根fiber是否存在,去执行mountChildFibers或者reconcileChildren:
reconcileChildren做的事情就是react的另一核心之一diff过程,在下一篇文章中会详细讲。
completeUnitOfWork
当workInProgress为null时,也就是当前任务的fiber树遍历完之后,就进入到了completeUnitOfWork函数。
经过了beginWork操作,workInProgress节点已经被打上了flags副作用标签。completeUnitOfWork方法中主要是逐层收集effects链,最终收集到root上,供接下来的commit阶段使用。
completeUnitOfWork结束后,render阶段便结束了,后面就到了commit阶段。
scheduler
实现帧空闲调度任务
浏览器会在每一帧空闲时刻去执行react更新任务,那么空闲时刻去执行是如何实现的呢?我们很容易联想到一个api——————requestldleCallback。但由于requestldleCallback的兼容性问题以及react对应部分高优先级任务可能牺牲部分帧的需要,react通过自己实现了类似的功能代替了requestldleCallback。
我们上面讲到执行可中断更新时,performConcurrentWorkOnRoot函数时通过scheduleCallback包裹起来的:
scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null,root),
)
scheduleCallback函数是引用了packages/scheduler/src/Scheduler.js 路径下的unstable_scheduleCallback函数,我们来看一下这个函数,他会去按计划插入调度任务:
将任务插入了调度队列之后,会通过requestHostCallback函数去调度任务。
来源:https://juejin.cn/post/7019254208830373902/
这篇关于react17.x源码解析(2)——fiber树的构建与更新的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-16Vue3资料:新手入门必读教程
- 2024-11-16Vue3资料:新手入门全面指南
- 2024-11-16Vue资料:新手入门完全指南
- 2024-11-16Vue项目实战:新手入门指南
- 2024-11-16React Hooks之useEffect案例详解
- 2024-11-16useRef案例详解:React中的useRef使用教程
- 2024-11-16React Hooks之useState案例详解
- 2024-11-16Vue入门指南:从零开始搭建第一个Vue项目
- 2024-11-16Vue3学习:新手入门教程与实践指南
- 2024-11-16Vue3学习:从入门到初级实战教程