Appearance
bailout
一个项目中往往包含非常庞大的 FiberNode 数,而我们的一次事件可能只是更新其中的一小部分分支,其他与当前更新不相关的分支就可以被忽略掉,这就是 bailout 的作用。
如何触发 bailout
在 beginWork 中,我们处理每一个 FiberNode 的时候都会进行一个非常多的条件去判断当前节点是否可以被复用,多余可以复用的节点就会被标记为 didReceiveUpdate = false, 然后再进行 attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes)
其判断条件如下
- 新老 props 相等, 这边是对象的地址比较
- context 改变了 没有发生改变
- 组件的类型没有改变
- FiberNode 节点不存在待更新的任务,也不存在上下文更新
如果都满足上述条件,那么就触发 billout 的下一个判断逻辑,因为可能当前节点上没有真正执行的 lane,但是其子节点存在正在执行的 lane,
所以在 billout 的判断中还会判断 !includesSomeLane(renderLanes, workInProgress.childLanes) , 即当前节点的子节点是否存在正在执行的 lane,
- 如果不存在,就会触发
bailout直接return null。 - 如果存在,就会触发
cloneChildFibers继续向下遍历。
具体看源码
js
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
// 如果存在 旧的节点,那么就直接复用旧节点依赖的 context 上下文对象
if (current !== null) {
// Reuse previous dependencies
workInProgress.dependencies = current.dependencies;
}
markSkippedUpdateLanes(workInProgress.lanes);
// Check if the children have any pending work.
// 判断当前节点及其子节点是否包含当前正在渲染的 lane
// 如果不包含,那么就直接返回 null
// 如果包含,那么就 cloneChildFibers 继续向下遍历
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.
lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
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.
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}如何知道子孙节点是否存在正在执行的 lane
在 React 中触发的更新主要是由以下几种方式 setState、useReducer、HostRoot等,在这些触发更新的方法中都存在一个逻辑 enqueueUpdate去创建一个 update 对象并放到 updateQueue 队列中,并对于在非渲染阶段触发的更新操作,执行 unsafe_markUpdateLaneFromFiberToRoot ,通过 fiber.return 向上遍历的方式将当前组件的 lane 标记到祖先组件的 childLanes 中,这样就可以通过 includesSomeLane(renderLanes, workInProgress.childLanes) 来判断当前节点及其子节点是否存在正在执行的 lane。
具体看源码
js
/**
* 通过 fiber.return 向上查找直到 root节点,
* 然后将当前update的lane合并到祖先节点的childLanes中 和
* 其备份节点 FiberNode.alternate 的 childLanes 中
* 1. 更新源 Fiber 的 lanes
* 2. 同步更新 alternate Fiber 的 lanes(双缓存机制)
* 3. 向上遍历父节点更新 childLanes
* @param {*} sourceFiber
* @param {*} update
* @param {*} lane
*/
function markUpdateLaneFromFiberToRoot(
sourceFiber: Fiber, // 触发更新的源 Fiber 节点
update: ConcurrentUpdate | null, // 并发更新对象
lane: Lane // 当前更新的优先级通道
): void {
// 1. 更新源 Fiber 的 lanes
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
// 2. 同步更新 alternate Fiber 的 lanes(双缓存机制)
let alternate = sourceFiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
// 3. 向上遍历父节点更新 childLanes
let isHidden = false;
let parent = sourceFiber.return;
let node = sourceFiber;
while (parent !== null) {
// 4. 更新父节点的 childLanes
parent.childLanes = mergeLanes(parent.childLanes, lane);
// 5. 同步更新 alternate 树的 childLanes
alternate = parent.alternate;
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
}
// 6. 检测隐藏的 Offscreen 组件
if (parent.tag === OffscreenComponent) {
const offscreenInstance: OffscreenInstance = parent.stateNode;
if (offscreenInstance.isHidden) {
isHidden = true;
}
}
// 7. 指针上移
node = parent;
parent = parent.return;
}
// 8. 处理隐藏更新
if (isHidden && update !== null && node.tag === HostRoot) {
const root: FiberRoot = node.stateNode;
markHiddenUpdate(root, update, lane);
}
}这里面有一个非常重要的点, 就是对于触发更新的上下文的不同 会进行不同的处理方式
- 为什么对于需要区分在不同阶段的更新操作,会有不同的处理逻辑?
- 在渲染阶段触发的更新
- 在非渲染阶段触发的更新
- 为什么会有两种不同的处理逻辑?
- 在渲染阶段阶段触发的更新,因为还没有进行 commit 操作,所以可以直接使用最新的一次更新作为最终的结果 所以我们在 beginWork 阶段直接依据节点本身去判断是否需要重新渲染子孙节点
- 在非渲染阶段触发的更新,因为在这个阶段是不可以被打断,所以这个阶段触发的更新是需要被存储起来的, 等到 commit 阶段再去处理,所以我们在这个阶段是将 update 对象存储到 concurrentQueue 队列中,并 将所有生成的更新的组件的 lane 合并到祖先节点中,commit 节点处理完成, 再次进行渲染阶段的时候,依据 lane,childLanes 去寻找一个个需要更新的节点线路
