这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」。
react 版本:v17.0.3
ClassComponent的三种更新情形
在《React 源码解读之class组件更新updateClassComponent (一)》一文中我们提到,对于 ClassComponent 的更新,有三种情形:
-
情形一:ClassComponent实例未被创建,此时会调用 constructClassInstance 方法构建class组件实例,然后调用 mountClassInstance 方法挂载class组件,并将 shouldUpdate 置为 true,标记组件需要更新渲染。
-
情形二:ClassComponent实例已经存在,但current(当前渲染在界面上的fiber树) 为null,即 ClassComponent 是初次渲染,此时调用 resumeMountClassInstance 方法,复用ClassComponent实例,并更新 state/props,然后会执行 componentWillMount 生命周期函数。
-
情形三:ClassComponent实例已经存在,且已经是多次渲染,此时调用 updateClassInstance 方法执行更新操作,且会执行 componentWillUpdate 生命周期函数。
执行完这三种更新后,updateClassComponent 最后执行了 finishClassComponent() 函数来判断是否需要 执行render 生命周期函数,即是否需要渲染组件。
// react-reconciler/src/ReactFiberBeginWork.new.js
// 判断是否执行 render 生命周期函数
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderLanes,
);
执行 render,渲染节点 -- finishClassComponent
finishClassComponent 做的事情就是判断 是否需要执行 render,渲染当前节点 ,并返回当前渲染节点的下一个节点。
// react-reconciler/src/ReactFiberBeginWork.new.js
function finishClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
shouldUpdate: boolean,
hasContext: boolean,
renderLanes: Lanes,
) {
// Refs should update even if shouldComponentUpdate returns false
// 无论 props/state 是否有更新,都需要更新 ref 指向
markRef(current, workInProgress);
// 错误捕获
const didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;
if (!shouldUpdate && !didCaptureError) {
// Context providers should defer to sCU for rendering
if (hasContext) {
invalidateContextProvider(workInProgress, Component, false);
}
// 当不需要更新或者已经更新完成,并且没有出现 error 的时候
// 跳过该 class 上的节点及所有子节点的更新,也就是跳过 render 方法
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// 获取class组件实例
const instance = workInProgress.stateNode;
// Rerender
ReactCurrentOwner.current = workInProgress;
let nextChildren;
//getDerivedStateFromError 是生命周期api,作用是捕获 render error
// 详情请看:https://zh-hans.reactjs.org/docs/react-component.html#static-getderivedstatefromerror
// 在 render 过程中如果出现了 error 但开发者没有调用 getDerivedStateFromError,则中断渲染
if (
didCaptureError &&
typeof Component.getDerivedStateFromError !== 'function'
) {
// If we captured an error, but getDerivedStateFromError is not defined,
// unmount all the children. componentDidCatch will schedule an update to
// re-render a fallback. This is temporary until we migrate everyone to
// the new API.
// TODO: Warn in a future release.
// 中断渲染
nextChildren = null;
if (enableProfilerTimer) {
stopProfilerTimerIfRunning(workInProgress);
}
} else {
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
// 执行渲染,即执行 render 生命周期函数
if (__DEV__) {
setIsRendering(true);
nextChildren = instance.render();
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictLegacyMode
) {
setIsStrictModeForDevtools(true);
try {
instance.render();
} finally {
setIsStrictModeForDevtools(false);
}
}
setIsRendering(false);
} else {
nextChildren = instance.render();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
}
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
if (current !== null && didCaptureError) {
// If we're recovering from an error, reconcile without reusing any of
// the existing children. Conceptually, the normal children and the children
// that are shown on error are two different sets, so we shouldn't reuse
// normal children even if their identities match.
// 重新计算 children,因为当出错时,是渲染到节点上的 state/props 出现了问题,所以不能复用,必须重新 render
forceUnmountCurrentAndReconcile(
current,
workInProgress,
nextChildren,
renderLanes,
);
} else {
// 进入协调过程
// 将 ReactElement 变成 fiber 对象,并更新,生成对应的 DOM 实例,并挂载到真正的 DOM 节点上
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
// Memoize state using the values we just used to render.
// TODO: Restructure so we never read values from the instance.
workInProgress.memoizedState = instance.state;
// The context might have changed so we need to recalculate it.
if (hasContext) {
invalidateContextProvider(workInProgress, Component, true);
}
// 返回下一个要渲染的节点
return workInProgress.child;
}
在 finishClassComponent 函数中,做了以下事情:
1、首先是更新 ref 指向,无论 props/state 是否有更新,都要更新 ref 的指向。代码如下:
// 无论 props/state 是否有更新,都需要更新 ref 指向
markRef(current, workInProgress);
2、根据fiber节点的 flags 判断是否有错误捕获,将其赋值给 didCaptureError 变量。代码如下:
// 错误捕获
const didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;
3、如果不需要更新或者已经更新完成,并且没有错误捕获时,执行 bailoutOnAlreadyFinishedWork 方法跳过该 class组件上的节点及其所有子节点的更新,也就是跳过class组件的 render 方法。代码如下:
if (!shouldUpdate && !didCaptureError) {
// Context providers should defer to sCU for rendering
if (hasContext) {
invalidateContextProvider(workInProgress, Component, false);
}
// 当不需要更新或者已经更新完成,并且没有出现 error 的时候
// 跳过该 class 上的节点及所有子节点的更新,也就是跳过 render 方法
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
4、在执行 render 的过程中,如果捕获到了 error,并且开发者也没有调用 getDerivedStateFromError 生命周期函数,则中断渲染,将 nextChildren 设为 null 。代码如下:
// 在 render 过程中如果出现了 error 但开发者没有调用 getDerivedStateFromError,则中断渲染
if (
didCaptureError &&
typeof Component.getDerivedStateFromError !== 'function'
) {
// If we captured an error, but getDerivedStateFromError is not defined,
// unmount all the children. componentDidCatch will schedule an update to
// re-render a fallback. This is temporary until we migrate everyone to
// the new API.
// TODO: Warn in a future release.
// 中断渲染
nextChildren = null;
if (enableProfilerTimer) {
stopProfilerTimerIfRunning(workInProgress);
}
}
5、如果没有捕获到 error,则执行class组件的 render 方法,渲染当前节点,并返回 nextChildren。代码如下:
else {
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
}
// 执行渲染,即执行 render 生命周期函数
if (__DEV__) {
// 删除了DEV 部分的代码
} else {
nextChildren = instance.render();
}
// ...
}
6、如果在渲染过程中捕获到了error,则执行 forceUnmountCurrentAndReconcile 函数,重新计算children。代码如下:
if (current !== null && didCaptureError) {
// 重新计算 children,因为当出错时,是渲染到节点上的 state/props 出现了问题,所以不能复用,必须重新 render
forceUnmountCurrentAndReconcile(
current,
workInProgress,
nextChildren,
renderLanes,
);
}
7、如果在渲染过程中没有error,则执行 reconcileChildren 函数进入协调过程,将 ReactElement 变成 fiber 对象并更新,生成对应的 DOM 实例,并挂载到真正的 DOM 节点上。代码如下:
else {
// 进入协调过程
// 将 ReactElement 变成 fiber 对象,并更新,生成对应的 DOM 实例,并挂载到真正的 DOM 节点上
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
8、最后更新workInProgress的memoizedState,然后返回当前渲染节点的下一个节点。代码如下:
// Memoize state using the values we just used to render.
// TODO: Restructure so we never read values from the instance.
workInProgress.memoizedState = instance.state;
// The context might have changed so we need to recalculate it.
if (hasContext) {
invalidateContextProvider(workInProgress, Component, true);
}
// 返回下一个要渲染的节点
return workInProgress.child;
接下来,我们来看看在 finishClassComponent 中调用的几个重要函数。
bailoutOnAlreadyFinishedWork
bailoutOnAlreadyFinishedWork 函数做的事情,就是跳过该 class组件上的节点及所有子节点的更新,也就是跳过 render 方法。
// react-reconciler/src/ReactFiberBeginWork.new.js
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
// Reuse previous dependencies
// 获取 current 树上的依赖,添加到当前工作的fiber上,从而复用之前的依赖
workInProgress.dependencies = current.dependencies;
}
if (enableProfilerTimer) {
// Don't update "base" render times for bailouts.
stopProfilerTimerIfRunning(workInProgress);
}
markSkippedUpdateLanes(workInProgress.lanes);
// Check if the children have any pending work.
// 检查子树是否需要更新,如果子树不需要更新,返回 null
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// The children don't have any work either. We can skip them.
// TODO: Once we add back resuming, we should check if the children are
// a work-in-progress set. If so, we need to transfer their effects.
if (enableLazyContextPropagation && current !== null) {
// Before bailing out, check if there are any context changes in
// the children.
// 在退出之前,检查子树是否有 context 发生变化
lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
// 子树 context 没有发行变化
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
return null;
}
} else {
return null;
}
}
// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
// 子树需要更新,拷贝一份 workInProgress.child 返回到 workLoop 的 nextUnitOfWork
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
可以看到,在 bailoutOnAlreadyFinishedWork 函数中,检查了子树是否需要更新,如果不需要更新,返回 null,跳过当前节点及子节点的更新,即跳过组件的render方法,不执行渲染。如果子树需要更新,则拷贝一份 workInProgress.child 返回到 workLoop 的 nextUnitOfWork。
forceUnmountCurrentAndReconcile
forceUnmountCurrentAndReconcile 函数做的事情,就是在渲染过程中出错时,重新计算 children,然后重新执行渲染。
// react-reconciler/src/ReactFiberBeginWork.new.js
function forceUnmountCurrentAndReconcile(
current: Fiber,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
// 执行两遍协调过程
// reconcileChildFibers 函数是 reconcileChildren 的副本,在 reconcileChildren 中会调用 reconcileChildFibers
// This function is fork of reconcileChildren. It's used in cases where we
// want to reconcile without matching against the existing set. This has the
// effect of all current children being unmounted; even if the type and key
// are the same, the old child is unmounted and a new child is created.
//
// To do this, we're going to go through the reconcile algorithm twice. In
// the first pass, we schedule a deletion for all the current children by
// passing null.
// 第一遍删除所有当前节点的所有子节点
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
null,
renderLanes,
);
// In the second pass, we mount the new children. The trick here is that we
// pass null in place of where we usually pass the current child set. This has
// the effect of remounting all children regardless of whether their
// identities match.
// 第二遍重新挂载所有的子节点
workInProgress.child = reconcileChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
}
在 forceUnmountCurrentAndReconcile 函数中,安排了两遍协调过程,第一遍是删除当前节点的所有子节点,第二遍是重新挂载所有的子节点。
reconcileChildFibers 函数可以理解为是 reconcileChildren 的副本,reconcileChildFibers 会在 reconcileChildren 函数中被调用,详情请阅读《React源码解读之Diff算法》中的「结合源码解读 diff」小节。
reconcileChildren
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
// mountChildFibers 和 reconcileChildFibers 都是调用 ChildReconciler函数
// mountChildFibers 调用 ChildReconciler函数时传入了false,表示不需要进行 diff,直接生成新的fiber
// reconcileChildFibers 调用 ChildReconciler函数时传入了true,表示需要进行 diff
if (current === null) {
// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
// 如果当前 fiber 节点 为空,则直接将新的 ReactElement 内容生成新的 fiber
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// If the current child is the same as the work in progress, it means that
// we haven't yet started any work on these children. Therefore, we use
// the clone algorithm to create a copy of all the current children.
// If we had any progressed work already, that is invalid at this point so
// let's throw it out.
// 当前fiber节点不为空,则与新生成的 ReactElement 内容进行diff
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
在渲染过程中没有error,会执行 reconcileChildren 函数进入协调过程。从源码中可以看到,在协调时,根据 current 是否存在,分别调用了mountChildFibers 函数和 reconcileChildFibers 函数。其实这两个方法调用的都是 ChildReconciler() 方法,只是传参不同。详情请阅读《React源码解读之Diff算法》中的「结合源码解读 diff」小节。
流程图
总结
本文是 React 源码解读之class组件更新updateClassComponent 系列的最后一篇。
updateClassComponent中,无论执行完哪种更新,最后都会执行 finishClassComponent 函数来判断是否需要 执行render 生命周期函数,即是否需要渲染组件。如果节点不需要更新,则执行 bailoutOnAlreadyFinishedWork 函数跳过更新,即跳过render方法。否则执行 render 方法执行更新。在执行更新的过程中,如果没有发生错误,则执行 reconcileChildren 函数进入协调流程,否则就中断渲染。