1. 前言
那么双缓存 Fiber 树是如何构建的?
在处理好优先级和标记后,就会进入到 render 阶段,此时仍在 Reconciler 中。取决于本次更新为同步还是异步,React 会调用 performSyncWorkOnRoot
或 performConcurrentWorkOnRoot
,前者为同步更新,后者为异步更新:
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
唯一的区别就是是否调用 shouldYield
方法,如果当前浏览器帧没有剩余时间,shouldYield
会中止循环,直到浏览器有空闲时间后再继续遍历。
workInProgress 就是当前已创建的 workInProgress fiber,performUnitOfWork 用于创建下一个 fiber 节点并赋值给 workInProgress,然后连接起来称为 Fiber 树。
在 React 16 架构中的 Fiber Reconciler
是从 Stack Reconciler
重构而来,通过遍历的方式实现可中断的递归,所以 performUnitOfWork
的工作可以分为两部分:“递”和“归”。
欢迎加入技术交流群。
2. 递阶段(beginWork
)
从 rootFiber 向下深度优先遍历,调用 beginWork
方法,将注入的 fiber 节点创建子 filer 节点,然后连接在一起,直到叶子节点(即没有子组件的组件)时进入“归”阶段。
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...省略函数体
}
- current:当前组件对应的 Fiber 节点在上一次更新时的 Fiber 节点,即
workInProgress.alternate
- workInProgress:当前组件对应的 Fiber 节点
- renderLanes:优先级相关
2.1 mount 时
代码细节方面,mount 时,会根据 fiber.tag
的不同创建不同子 Fiber 节点:
// mount时:根据tag不同,创建不同的子Fiber节点
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case FunctionComponent:
// ...省略
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他类型
}
2.2 update 时
如果是 update 时,如果 current 树不为空,那么可以复用之前的 current Fiber,比如克隆 current.child
为 workInProgress.child
,这样就不需要重复创建:
// update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
if (current !== null) {
// ...省略
// 复用current
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
didReceiveUpdate = false;
}
2.3 reconcileChildren
reconcileChildren 方法是 Reconciler 的核心部分:
- 对于 mount 的组件:创建子 Fiber 节点
- 对于 update 的组件:对比当前和上一次更新的 Fiber 节点(Diff 算法),然后生成新的 Fiber 节点。
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
// 对于mount的组件
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
// 对于update的组件
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}
它在 beginWork
执行后调用,返回值为一个新的 Fiber 节点,最终都会给到 workInProgress.child
,并作为下一次 performUnitOfWork
执行时的 workInProgress
传参。
reconcileChildFibers
方法执行后会为节点添加 effectTag
属性。
fiber.effectTag
保存了需要在 Renderer 中执行的 DOM 操作:
// DOM需要插入到页面中
export const Placement = /* */ 0b00000000000010;
// DOM需要更新
export const Update = /* */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /* */ 0b00000000000110;
// DOM需要删除
export const Deletion = /* */ 0b00000000001000;
3. 归阶段(completeWork
)
调用 completeWork
方法处理 fiber 节点。如果该节点存在兄弟节点,则进入它的“递”阶段;如果不存在就进入父节点的“归”阶段,这样交错进行直到 rootFiber
。
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...省略函数体
}
在这个阶段,仍然会根据 fiber.tag
的不同执行不同的逻辑:
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
case ClassComponent: {
// ...省略
return null;
}
case HostRoot: {
// ...省略
updateHostContainer(workInProgress);
return null;
}
case HostComponent: {
// ...省略
return null;
}
}
根据 current === null ?
判断 mount 还是 update,上面的 HostComponent 这种类型表示原生 DOM 元素,例如,如果一个 Fiber 节点是一个 <div>
元素的 HostComponent
,那么这个节点的 stateNode
属性会指向相应的 DOM 元素。因此,不仅要考虑 current
是否为 null,还要考虑 fiber.stateNode
是否有值:
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// update的情况
// ...省略
} else {
// mount的情况
// ...省略
}
return null;
}
3.1 mount 时
递的时候,会根据 Fiber 节点生成 DOM 节点,将子孙 DOM 节点插入刚生成的 DOM 节点中,然后处理 props。
// mount的情况
// ...省略服务端渲染相关逻辑
const currentHostContext = getHostContext();
// 为fiber创建对应DOM节点
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress
);
// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false);
// DOM节点赋值给fiber.stateNode
workInProgress.stateNode = instance;
// 与update逻辑中的updateHostComponent类似的处理props的过程
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext
)
) {
markUpdate(workInProgress);
}
3.2 update 时
在 mount 时已经生成了对应的 DOM 节点,所以 update 阶段就不需要这步操作。需要做的是处理 props:
onClick
、onChange
等回调函数的注册- 处理
style prop
- 处理
DANGEROUSLY_SET_INNER_HTML prop
- 处理
children prop
if (current !== null && workInProgress.stateNode != null) {
// update的情况
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance
);
}
需要注意的是,在 updateHostComponent
内部有一句:
workInProgress.updateQueue = (updatePayload: any);
这意味着被处理过的(变化的) props
被挂在 fiber.updateQueue
上,后续在 commit 阶段就会被渲染在页面上。
3.3 appendAllChildren
在 mount 时有一个 appendAllChildren
方法,它的调用会不断将子孙 DOM 节点插入到当下的 DOM 节点中,因此当到达 rootFiber 时,就已经有了一个完整的离屏 DOM 树了,然后提交给 Renderer 渲染就行了。
4. effectList 与单向链表
在有了完整的 DOM 树,并且 props 也都被处理完毕,在进入 commit 阶段之前有一个问题。
在渲染时,要考虑到每个节点的 effectTag
(递阶段追加上的执行的标记操作),如果还是递归的方式去查找带有该属性的标记节点就会陷入复杂的逻辑中。
React 在这块的处理办法是利用单向链表的方式:
nextEffect nextEffect nextEffect
rootFiber.firstEffect ----------> fiber ----------> fiber ----------> fiber.lastEffect
在归阶段 Fiber 节点执行 completeWork
后,如果存在 effectTag
属性就会被保存在一条被称为effectList
的单向链表中。
这样,只要遍历整条链表就能执行所有的 effect 了。
effectList
相较于Fiber树
,就像圣诞树上挂的那一串彩灯。—— React 团队成员 Dan Abramov
5. commit 阶段(渲染页面)
在 render 阶段处理完所有的工作后,fiberRoot 传递给 commitRoot
方法,进入 commit 阶段。
与 render 阶段(异步可中断)不同,这个阶段是同步执行的,React 将会根据 rootFiber.firstEffect
以及每个 fiber 下的 updateQueue
属性来渲染。
rootFiber.firstEffect
:保存了副作用的effectList
fiber.updateQueue
:保存了变化的props
另外,除了上述属性的 DOM 操作,在这个阶段还会执行生命周期钩子、hook。
5.1 主要工作
这个阶段的主要工作:
- 变量赋值、状态重制
- before mutation 阶段(执行 DOM 操作前)
- mutation 阶段(执行 DOM 操作)
- layout 阶段(执行 DOM 操作后)
- useEffect 相关处理、性能追踪、生命周期钩子和 hook
5.2 before mutation
在这个阶段,React 会执行一些生命周期方法。
遍历 effectList
,调用 commitBeforeMutationEffects
:
- 处理 DOM 节点渲染/删除后的
autoFocus
、blur
逻辑 - 调用
getSnapshotBeforeUpdate
生命周期钩子 - 调度
useEffect
由于 React 15 和 React 16 架构上的不同,组件在渲染时可能会多次触发 componentWillXXX
生命周期钩子。React 提供了替代的生命周期钩子 getSnapshotBeforeUpdate
。
有源码如下:
// 调度useEffect
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
// 触发 useEffect
flushPassiveEffects();
return null;
});
}
}
scheduleCallback
方法属于 Scheduler 调度器模块,以某个优先级异步调度一个回调函数。
在回调函数中可以看到 flushPassiveEffects
方法会触发 useEffect
。flushPassiveEffects
方法会拿到 effectList
链表,之后会遍历执行链表,于是副作用就会触发。
为什么要异步调度?
- 保证 DOM 更新和副作用顺序:在 React 的架构中,DOM 更新是在 Commit 阶段同步完成的。为了确保 DOM 完成更新之后再执行副作用,
flushPassiveEffects
被设计成异步的,这样可以保证所有的 DOM 变更都已完成,然后才开始执行副作用。这避免了在中间状态执行副作用,从而确保副作用依赖的 DOM 状态是最新的。 - 避免嵌套更新导致的无限循环:如果副作用同步执行且在执行过程中触发了新的状态更新,可能会导致嵌套的更新循环,特别是在副作用中调用了
setState
。异步调度副作用可以有效避免这种情况,确保所有的更新在一个批次中完成后,再处理副作用。
5.3 mutation
遍历 effectList
执行,调用 commitMutationEffects
:
- 根据
ContentReset effectTag
重置文字节点 - 更新
ref
- 根据
effectTag
分别处理,其中effectTag
包括(Placement
|Update
|Deletion
|Hydrating
)
简单来说,React 会将所有需要更新的 DOM 变更应用到真实的 DOM 上。
5.4 layout
遍历 effectList
执行,调用 commitLayoutEffects
:
commitLayoutEffectOnFiber
(调用生命周期钩子
和hook
相关操作)commitAttachRef
(赋值 ref)
-
对于
commitLayoutEffectOnFiber
:根据
fiber.tag
的不同:如果是 ClassComponent,会通过current === null?
区分是mount
还是update
,调用componentDidMount
****或componentDidUpdate
;如果是 FunctionComponent 相关类型,会调用useLayoutEffect hook
的回调函数,调度useEffect
的销毁与回调函数。 -
对于
commitAttachRef
:获取 DOM 实例,更新ref
。
简单来说,该方法的主要工作就是根据 effectTag
调用不同的处理函数处理 Fiber
并更新 ref
。
6. 总结
在 render 阶段遍历组件树,调用每个组件的 render 方法,根据返回的 JSX 元素创建出对应的 Fiber 节点并连接起来,挂载和更新的阶段是不同的,对于更新的节点,只要状态更新、props 改变,就会触发计算生成新的树,期间如果有更高优先级的任务就会中断让位,通过调度器来确定在何时恢复 render 阶段的工作。
在提交到 Render 之前,就生成好了一个完整的 DOM 树,不过是在内存中的,通过 effectList 将副作用连接起来,commit 阶段主要通过遍历这个 effectList 来触发生命周期函数getSnapshotBeforeUpdate
、渲染真实 DOM 以及触发 componentDidUpdate
、componentDidMount
生命周期钩子或处理相关副作用函数。