多种不同类型的组件的更新过程,以及如何遍历节点形成新的 Fiber 树,即 reconcilerChildren 调和子节点的过程。
-1. 入口和优化
- 判断组件更新是否可以优化
- 根据节点类型分发处理
- 根据 expirationTime 等信息判断是否可以跳过
帮助优化整个树的更新过程的方法。
只有 ReactDOM.render() 的时才会更新 RootFiber,其后的更新都是在子节点上。
workLoop:
function workLoop(isYieldy) {
if (!isYieldy) {
// Flush work without yielding
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {
// Flush asynchronous work until the deadline runs out of time.
while (nextUnitOfWork !== null && !shouldYield()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
}
performUnitOfWork:更新子树,调用了 beginWork:
function performUnitOfWork(workInProgress: Fiber): Fiber | null {
const current = workInProgress.alternate;
// See if beginning this work spawns more work.
startWorkTimer(workInProgress);
let next;
if (enableProfilerTimer) {
if (workInProgress.mode & ProfileMode) {
startProfilerTimer(workInProgress);
}
next = beginWork(current, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
if (workInProgress.mode & ProfileMode) {
// Record the render duration assuming we didn't bailout (or error).
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
}
} else {
// 这里返回子节点
next = beginWork(current, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
}
if (next === null) {
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(workInProgress);
}
ReactCurrentOwner.current = null;
return next;
}
beginWork:
- 判断如果是非首次渲染(current !== null):
新老 props 一样,而且本次更新任务的优先级并没有超过现有任务的最高优先级,则做一些优化的工作,然后调用 xxx 用于跳过当前 Fiber 树及其子节点的所有更新。
- 然后可能是非首次但没能跳过,也可能仍然是首次渲染(代码太多,没贴)。
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
const updateExpirationTime = workInProgress.expirationTime;
// 传入的 current,第一次渲染
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps === newProps &&
!hasLegacyContextChanged() &&
(updateExpirationTime === NoWork ||
updateExpirationTime > renderExpirationTime)
) {
// 处理不同类型的节点
// This fiber does not have any pending work. Bailout without entering
// the begin phase. There's still some bookkeeping we that needs to be done
// in this optimized path, mostly pushing stuff onto the stack.
switch (workInProgress.tag) {
case HostRoot:
// 太多,暂略
}
// 用于跳过子节点的更新
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
}
// 然后可能是非首次但没能跳过,也可能仍然是首次渲染(代码太多,没贴)。
}
bailoutOnAlreadyFinishedWork:
用于跳过子节点的更新。
但也要看任务优先级也不紧急的话,就函数返回 null,外部的 while 遍历就停止了,也就跳过了所有子组件的更新。
但如果优先级更高的话,则克隆 current 上面的 child 并返回,然后再返回到 workLoop 中,进入下次 child 更新循环,去尝试更新子节点。这就是个不断向下遍历节点的过程。
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
cancelWorkTimer(workInProgress);
if (current !== null) {
// Reuse previous context list
workInProgress.firstContextDependency = current.firstContextDependency;
}
if (enableProfilerTimer) {
// Don't update "base" render times for bailouts.
stopProfilerTimerIfRunning(workInProgress);
}
// Check if the children have any pending work.
const childExpirationTime = workInProgress.childExpirationTime;
if (
childExpirationTime === NoWork ||
childExpirationTime > renderExpirationTime
) {
// 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.
return null;
} else {
// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
}
0. 各种不同类型组件的更新
先说整体概念,既然是不同类型的组件更新,因此关注的粒度就是在一整棵 fiber 树中,某一层是某一种类型的组件,其上的更新。而其子组件的更新,会在下一次 workLoop 遍历的时候再真正处理。
接下来是各种组件类型的更新,也就是调和 Fiber 子节点的过程。
在 react-reconciler/ReactFiberBeginWork.js/beginWork() 方法中:
Fiber 上的 tag 标记了不同的组件类型,在这里用作 switch 的判断,根据不同组件类型分别进行 fiber 的调和更新:
switch (workInProgress.tag) {
case IndeterminateComponent: {
const elementType = workInProgress.elementType;
return mountIndeterminateComponent(
current,
workInProgress,
elementType,
renderExpirationTime,
);
}
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
updateExpirationTime,
renderExpirationTime,
);
}
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,
renderExpirationTime,
);
}
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,
renderExpirationTime,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderExpirationTime);
case HostComponent:
return updateHostComponent(current, workInProgress, renderExpirationTime);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(
current,
workInProgress,
renderExpirationTime,
);
case HostPortal:
return updatePortalComponent(
current,
workInProgress,
renderExpirationTime,
);
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,
renderExpirationTime,
);
}
case Fragment:
return updateFragment(current, workInProgress, renderExpirationTime);
case Mode:
return updateMode(current, workInProgress, renderExpirationTime);
case Profiler:
return updateProfiler(current, workInProgress, renderExpirationTime);
case ContextProvider:
return updateContextProvider(
current,
workInProgress,
renderExpirationTime,
);
case ContextConsumer:
return updateContextConsumer(
current,
workInProgress,
renderExpirationTime,
);
case MemoComponent: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps = resolveDefaultProps(type.type, unresolvedProps);
return updateMemoComponent(
current,
workInProgress,
type,
resolvedProps,
updateExpirationTime,
renderExpirationTime,
);
}
case SimpleMemoComponent: {
return updateSimpleMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
updateExpirationTime,
renderExpirationTime,
);
}
case IncompleteClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return mountIncompleteClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
default:
invariant(
false,
'Unknown unit of work tag. This error is likely caused by a bug in ' +
'React. Please file an issue.',
);
}
1. Function component 的更新
updateFunctionComponent:
之前说过每个 Fiber 节点上的 type 就是指 createReactElement 时传入第一个参数,即 函数/class/原生dom标签字符串/内置的某些类型(如React.Fragment 什么的,大多数时候会是个 symbol 标记)。
所以从 type 上获取对应的组件函数,传入 nextProps 和 context 执行后获取 nextChildren,也就是函数组件返回的东西,作为自己的 children。
但是 children 是 react element,因此需要还需要调用 reconcileChildren 涉及到 转化为 Fiber 对象和更新等。
然后返回 workInProgress.child,因为刚才 reconcileChildren 时会把处理好的 fiber 挂载到 child 上。
函数组件的更新如此看来是比较简单的,主要复杂的地方在 reconcileChildren 的过程中。
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderExpirationTime,
) {
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
const context = getMaskedContext(workInProgress, unmaskedContext);
let nextChildren;
prepareToReadContext(workInProgress, renderExpirationTime);
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
ReactCurrentFiber.setCurrentPhase('render');
nextChildren = Component(nextProps, context);
ReactCurrentFiber.setCurrentPhase(null);
} else {
// 这里调用函数组件,传入props和context,等到该函数组件的子 element 树。
nextChildren = Component(nextProps, context);
}
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
// 复杂的在这个方法中
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
return workInProgress.child;
}
2. reconcileChildren
- 根据 reactElement 上的 props.children 生成 fiber 子树。
- 判断 Fiber 对象是否可以复用。因为只有第一次是整体全部渲染,而后续更新时自然要考虑复用。
- 列表根据 key 优化。
- 最终迭代处理完整个 fiber 树。
调和子节点,主要分为第一次渲染,和后续更新。二者区别通过变量 shouldTrackSideEffects “是否追踪副作用” 来区分,也就是非第一次渲染,会涉及到相关副作用的处理和复用。
reconcileChildren:
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderExpirationTime: ExpirationTime,
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderExpirationTime,
);
}
}
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
props.children 中的合法的成员主要就是 数组/字符串/数字 以及 react element。
- React.Fragment 的是临时的节点,渲染更新时要被跳过,newChild = newChild.props.children,也就是把下一层的 children 赋值为当前某个待更新的组件的 children。
- 找到可复用的节点进行 return,而不可复用的节点,也就是 key 变了,就不会复用老的 fiber,老的 fiber 被删除。就涉及到重新创建子节点。
而重新创建子节点时要看子节点的类型:
对于 REACT_ELEMENT_TYPE:
在 reconcileSingleElement 中根据不同的组件类型,得到不同的 fiberTag,然后调用 createFiber(fiberTag, pendingProps, key, mode) 创建创建不同的 Fiber。
对于 string 或者 number,也就是文本节点:
只看第一个节点是不是文本节点:
- 如果此前老的第一个子节点也是文本节点,那么就复用留着,而删除相邻节点,因为现在要更新为文本节点了,所以留一个节点就够用了。
- 如果不是,那么就整个删除老的子节点。
对于 Array 或者 IteratorFn(有迭代器的函数):下一节再说。
**
如果以上情况都不符合,那就全部当做非法(我编的术语)子节点,因此就将其全部“删除”即可。
嘴上说着删除,但实际上,不能真的删,现在是在 workInProgress fiber 树上进行更新操作,并不会真的删除 dom,而只是打相应的标记,是删除操作?那就给 fiber 节点打上 Deletion
标记,也就是:
childToDelete.effectTag = Deletion。
之前说过更新分两个阶段,render (有可能被打断) 和 commit (不会被打断) 阶段,在 render 阶段为这些 fiber 打上相应的操作标记后,在后面的 commit 阶段在根据这些标记,去真正的操作浏览器 dom。
3. key 和数组调和
- key 的作用。作为对比判断依据,从而尽量复用老的 fiber 节点。
- 对比数组 children 是否可复用。
- generator 和 Array 的区别,基本差不多,只是前者是 ES6 迭代器相关知识,需要不断调用 next() 来获取成员。
使用 react 时如果返回的是数组(如使用 Array.prototype.map),需要为每个子项指定 key 值。
**
以相同顺序分别遍历新老 children,对比 key 是否相同 来决定是否复用老的 fiber 节点:
直到遇到 key 开始不相同了,就不再对标着复用,而此时 props.children 也就是 react element 的数组还有剩余,也就是还没全部转化为 fiber。那么有两种情况:
- 对位的老的子节点 oldFiber 已经用完了,那么就为剩余未转换的 react element 每个都单独创建 fiber 对象。
- 如果 oldFiber 还有剩余,只是一一对位的 key 开始变得和新的 key 不匹配,所以才打断了第一阶段的复用。但其实还有机会进行复用,可以遍历剩余的 oldFiber,以其 key 作为 Map 数据结构的 key,进行存储。然后看新的 key 是否能从 Map 中找到相应的 oldFiber,以便进行复用。这说明本次更新中,某个节点是位置只是位置顺序变了。还是可以找到并复用的。Map 中剩余的就是真的没用了,就标记为删除。
4. ClassComponent
在 react hooks 出现之前,唯一能引起二次更新的方法,就是 class 实例上的 setState 和 forceUpdate
- 计算新的 state:会使用 Object.assign({}, preState, particalState),用局部 state 对 preState 进行浅覆盖,来生成新的 state。
- 在 class 实例上,分别根据初次渲染还是后续更新来调用不同的生命周期方法。
5. IndeterminateComponent
在最初第一次渲染时,对于所有的 functionalComponent 都初始标记为 IndeterminateComponent 类型,
然后主要根据其返回的 value 中是否有 render 方法,从而才将 workInProgress.tag 其进一步明确为 ClassComponent 还是 FunctionComponent。
基于内部这种判断逻辑,我们竟然可以通过在函数式组件中返回的对象上提供 render 函数,以此将函数式组件“模拟”出了 class 组件的形式。这算是个小 hack 技巧,实际中应该没人这么干。
import React from 'react'
export default function TestIndeterminateComponent() {
return {
componentDidMount() {
console.log('invoker')
},
render() {
return <span>aaa</span>
},
}
}
6. HostRoot
该特殊类型对应的是 FiberRoot 节点。
7. HostComponent & HostText
- HostComponent:原生 dom 节点,也就是 jsx 中小写的那种。
- HostText:文本节点。
8. PortalComponent
独特地方在于其需要有单独的挂载点。
9. ForwardRef
- 下次更新传入的 ref 如果没变化,会跳过当前节点的更新(
bailoutOnAlreadyFinishedWork
)。 - 要注意被 ForwardRef 包裹后的组件内部获取不到外部提供的 context。
- 然后同样是调和子节点,根据调用 render 得到新的 react element,调和为相应的 fiber 节点。
10. Mode
- ConCurrentMode
- StrictMode
这样的组件类型其实只是一种标记,在 Fiber 的 mode 属性(通过位运算)上进行记录,在后面的创建更新时,mode 作为计算不同的 expirationTime 的依据。
11. MemoComponent
本质的更新逻辑和 FunctionalComponent 一样,只是多了一步对新老 props 的 shallowEqual 浅比较,从而有机会跳过本次更新。
LazyComponent 和 SuspenseComponent 后面单独研究。
网友评论