下面我们结合源码,来看一下实际工作过程中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/