React 知命境第 43 篇,原创第 156 篇
无论是并发模式,还是同步模式,最终要生成新的 Fiber Tree,都是通过遍历 workInProgress
的方式去执行 performUnitOfWork
// 并发模式
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
代码语言:javascript复制// 同步
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
代码语言:javascript复制需要特别注意的是这里的 workInProgress 表示当前正在执行的 Fiber 节点,他会在递归的过程中不断改变指向,这里要结合我们之前章节中分享过的
Fiber Tree
的链表结构来理解
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
performUnitOfWork
该方法主要用于创建 Fiber Tree,是否理解 Fiber Tree 的构建过程,跟我们是否能做好性能优化有非常直接的关系,因此对我而言,这是 React 源码中最重要的一个部分。
从他的第一行代码我们就能知道,Fiber Tree 的创建是依赖于双缓存策略。上一轮构建完成的 Fiber tree,在代码中用 current
来表示。
正在构建中的 Fiber tree,在代码中用 workInProgress
来表示,并且他们之间同层节点都用 alternate
相互指向。
current.alternate = workInProgress;
workInProgress.alternate = current;
workInProgress
会基于 current
构建。
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
...
整体的思路是从 current[rootFiber]
树往下执行深度遍历,在遍历的过程中,会根据 key、props、context、state
等条件进行判断,判断结果如果发现节点没有发生变化,那么就复用 current
的节点,如果发生了变化,则重新创建 Fiber 节点,并标记需要修改的类型,用于传递给 commitRoot
beginWork
每一个被遍历到的 Fiber 节点,会执行 beginWork
方法
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
该方法根据传入的 Fiber 节点创建子节点,并将这两个节点连接起来
代码语言:javascript复制function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {...}
React 在 ReactFiberBeginWork.new.js
模块中维护了一个全局的 didReceiveUpdate
变量,来表示当前节点是否需要更新
let didReceiveUpdate: boolean = false;
在 beginWork 的执行过程中,会经历一些判断来确认 didReceiveUpdate
的值,从而判断该 Fiber 节点是否需要重新执行
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
// Force a re-render if the implementation changed due to hot reload:
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// If props or context changed, mark the fiber as having performed work.
// This may be unset if the props are determined to be equal later (memo).
didReceiveUpdate = true;
} else {
这里比较的是 props 和 context 是否发生了变化。当他们其中一个变化时,则将 didReceiveUpdate
设置为 true
这里的 hasLegacyContextChanged() 兼容的是旧版本 的 context,新版本的 context 是否发生变化会反应到 pending update 中,也就是使用下面的
checkScheduledUpdateOrContext
来查看是否有更新的调度任务
当 props 和 context 都没有发生变化,并且也不存在对应的调度任务时,将其设置为 false
代码语言:javascript复制如果有 state/context 发生变化,则会存在调度任务
} else {
// Neither props nor legacy context changes. Check if there's a pending
// update or context change.
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
// If this is the second pass of an error or suspense boundary, there
// may not be work scheduled on `current`, so we check for this flag.
(workInProgress.flags & DidCapture) === NoFlags
) {
// No pending updates or context. Bail out now.
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
这里有一个很关键的点,就在于当方法进入到 attemptEarlyBailoutIfNoScheduledUpdate 去判断子节点是否可以 bailout 时,他并没有比较子节点的 props
核心的逻辑在 bailoutOnAlreadyFinishedWork
中。
{
...
// 判断子节点是否有 pending 任务要做
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// The children don't have any work either. We can skip them.
return null
}
// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
所以这里有一个很重要的思考就是为什么判断子节点是否发生变化时,并没有去比较
props
,这是性能优化策略的关键一步,结合我们之前讲的性能优化策略去理解,你就能知道答案
回到 beginWork, 后续的逻辑会根据不同的 tag
,创建不同类型的 Fiber 节点
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
renderLanes,
);
}
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case HostPortal:
return updatePortalComponent(current, workInProgress, renderLanes);
case ForwardRef: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === type
? unresolvedProps
: resolveDefaultProps(type, unresolvedProps);
return updateForwardRef(
current,
workInProgress,
type,
resolvedProps,
renderLanes,
);
}
// ...
case MemoComponent: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
let resolvedProps = resolveDefaultProps(type, unresolvedProps);
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
const outerPropTypes = type.propTypes;
if (outerPropTypes) {
checkPropTypes(
outerPropTypes,
resolvedProps, // Resolved for outer only
'prop',
getComponentNameFromType(type),
);
}
}
}
resolvedProps = resolveDefaultProps(type.type, resolvedProps);
return updateMemoComponent(
current,
workInProgress,
type,
resolvedProps,
renderLanes,
);
}
}
// ... 其他类型
我们重点关注 updateFunctionComponent
的执行逻辑,可以发现,当 didReceiveUpdate
为 false 时,会执行 bailout
跳过创建过程
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
如果无法 bailout,则最后执行 reconcileChildren
创建新的子节点
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
另外我们还应该关注 updateMemoComponent
中的逻辑。该逻辑通过浅比较函数 shallowEqual
来比较更新前后两个 props 的差异。当比较结果为 true 时,也是调用 bailout
跳过创建。
代码语言:javascript复制而不是沿用 didReceiveUpdate 的结果
if (!hasScheduledUpdateOrContext) {
// This will be the props with resolved defaultProps,
// unlike current.memoizedProps which will be the unresolved ones.
const prevProps = currentChild.memoizedProps;
// Default to shallow comparison
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
completeWork
在 performUnitOfWork 执行过程中,当发现当前节点已经没有子节点了,就会调用 completeUnitOfWork
方法
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
该方法主要用于执行 completeWork
。completeWork 主要的作用是用于创建与 Fiber 节点对应的 DOM 节点。
代码语言:javascript复制这里创建的 DOM 节点并没有插入到 HTML 中,还存在于内存里
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
appendAllChildren(instance, workInProgress, false, false);
由于 completeWork
的执行是从叶子节点,往根节点执行,因此,每次我们将新创建的节点 append
到父节点,执行到最后 rootFiber 时,一个完整的 DOM 树就已经构建完成了
completeWork 的执行顺序是一个回溯的过程
当然,Fiber 节点与 DOM 节点之间,也会保持一一对应的引用关系,因此在更新阶段,我们能够轻易的判断和复用已经存在的 DOM 节点从而避免重复创建
遍历顺序
beginWork
和 completeWork
的执行顺序理解起来比较困难,为了便于理解,我们这里用一个图示来表达
例如有这样一个结构的节点
代码语言:javascript复制<div id="root">
<div className="1">
<div className="1-1">1-1</div>
<div className="1-2">1-2</div>
<div className="1-3">
<div className="1-3-1">1-3-1</div>
</div>
</div>
<div className="2">2</div>
<div className="3">3</div>
</div>
beginWork
的执行是按照 Fiber 节点的链表深度遍历执行。
completeWork
则是当 fiber.next === null
时开始执行,他一个从叶子节点往根节点执行的回溯过程。当叶子节点被执行过后,则对叶子节点的父节点执行 completeWork
下图就是上面 demo 的执行顺序
其中蓝色圆代表对应节点的 beginWork
执行。黄色圆代表对应节点的 completeWork
执行。
总结
beginWork
与 completeWork
的执行是 React 源码中最重要的部分,理解他们的核心逻辑能有效帮助我们做好项目的性能优化。因此在学习他们的过程中,应该结合实践去思考优化策略。
不过性能优化的方式在我们之前的章节中已经详细介绍过,因此这里带大家阅读源码更多的是做一个验证,去揭开源码的神秘面纱。
到这篇文章这里,React 原理的大多数重要逻辑我们在知命境的文章都已经给大家分享过了,其中包括同步更新逻辑,异步更新逻辑,任务优先级队列,任务调度,Fiber 中的各种链表结构,各种比较方式的成本,包括本文介绍的 Fiber tree 的构建过程,大家可以把这些零散的文章串起来总结一下,有能力的可以自己在阅读源码时结合我分享的内容进一步扩展和完善。
阅读源码是一个高投入,低回报的过程,希望我的这些文章能有效帮助大家以更低的时间成本获得更高的知识回报。