Skip to content

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 中触发的更新主要是由以下几种方式 setStateuseReducerHostRoot等,在这些触发更新的方法中都存在一个逻辑 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);
  }
}

这里面有一个非常重要的点, 就是对于触发更新的上下文的不同 会进行不同的处理方式

  1. 为什么对于需要区分在不同阶段的更新操作,会有不同的处理逻辑?
  • 在渲染阶段触发的更新
  • 在非渲染阶段触发的更新
  1. 为什么会有两种不同的处理逻辑?
  • 在渲染阶段阶段触发的更新,因为还没有进行 commit 操作,所以可以直接使用最新的一次更新作为最终的结果 所以我们在 beginWork 阶段直接依据节点本身去判断是否需要重新渲染子孙节点
  • 在非渲染阶段触发的更新,因为在这个阶段是不可以被打断,所以这个阶段触发的更新是需要被存储起来的, 等到 commit 阶段再去处理,所以我们在这个阶段是将 update 对象存储到 concurrentQueue 队列中,并 将所有生成的更新的组件的 lane 合并到祖先节点中,commit 节点处理完成, 再次进行渲染阶段的时候,依据 lane,childLanes 去寻找一个个需要更新的节点线路

参考

优化的工作路径 —— bailout