2023 年,你是时候要掌握 react 的并发渲染了(4) - lane 模型(2)

lane 模型的用途

参与任务执行权的竞争 - getNextLanes()

这无疑是 lane 的第一个要讲的用途。无论是用户触发的更新请求还是 react 内部尝试去执行一次界面更新流程,这两者的交互点都是同一个函数: ensureRootIsScheduled

在 react 的内部,它会在很多场景下它会去尝试执行一次界面更新流程。全局搜索对 ensureRootIsScheduled() 的调用,你会发现有 15 次之多。

对于 ensureRootIsScheduled() 函数作用的描述,源码中的注释已经给出说明了:

Use this function to schedule a task for a root. There's only one task per root; if a task was already scheduled, we'll check to make sure the priority of the existing task is the same as the priority of the next level that the root has work on. This function is called on every update, and right before exiting a task.

从注释中,我们知道,react 会在两种情况下调用这个函数:

  • 用户触发的更新请求;
  • 当前单次任务执行完(并不是说一定是「完全」执行完)之后,退出之前;

这里还要说明的一点是,「schedule a task」里面的「task」其实绝大部分情况下就是指两个 react 界面更新流程的正式入口函数之一:

  • performSyncWorkOnRoot() 或者;
  • performConcurrentWorkOnRoot()

那到底是执行 performSyncWorkOnRoot() 还是执行 performConcurrentWorkOnRoot() 呢?还是说干脆什么都不做呢?react 的判断依据就是 lane - 一个从 root.pendingLanes 身上 resolve 出来的 lane(root.pendingLanes 上面我们已经了解过了,它代表的是当前累积的还没有执行完成的所有类型的任务)。到这里就引出了我们这小节的主角 - getNextLanes()getNextLanes()实现了从众多待执行任务里面找出要下一个渲染周期中要执行的一种或者多种类型任务的算法。

getNextLanes() 在 react 的内部会有几个应用场景。而在ensureRootIsScheduled() 函数里面的应用是最重要的一个。值得再次强调的是,如 getNextLanes() 函数名所反映的那样,它的返回值是 lanes。而在 ensureRootIsScheduled() 中我们只需要关注一个 lane,那 react 是如何从 lanes 得到 lane 呢? 那就是调用 getHighestPriorityLane()来从 lanes 获取最高优先级的那个 lane。总的来说,getNextLanes() 会返回 lanes,而 lanes 有下面几个用途:

  • 继续基于 lanes 来取出具有最高优先级的 lane, 用它来决定是否需要发出一个新的「任务调度请求」(这种用法具体就是指在 ensureRootIsScheduled() 里面,上面已经讲述过了);

  • 用它来判断是否还有待执行的任务,用法:

    js 复制代码
    if (nextLanes === NoLanes){
        // do something
    }
    或者
     if (nextLanes !== NoLanes){
        // do something
    }

    这种请用场景主要是指 performConcurrentWorkOnRoot() 里面,它在进入 concurrent work loop 之前会有一个防守:

    js 复制代码
    function performConcurrentWorkOnRoot(root, didTimeout) {
        ...
        let lanes = getNextLanes(
        root,
        root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
        );
    
        if (lanes === NoLanes) {
        // Defensive coding. This is never expected to happen.
        return null;
        }
        ...
    }
  • 用它来判断是否含有某种类型的任务。比如说,在performSyncWorkOnRoot() 里面,它也会做一个防守,如果当前没有同步类型任务了,那么 react 就不会继续进入 sync work loop(调用 renderRootSync()):

    js 复制代码
    function performSyncWorkOnRoot(root) {
        ...
        let lanes = getNextLanes(root, NoLanes);
        if (!includesSomeLane(lanes, SyncLane)) {
        // There's no remaining sync work left.
        ensureRootIsScheduled(root, now());
        return null;
        }
        let exitStatus = renderRootSync(root, lanes);
        ...
    }

    上面介绍了 getNextLanes() 的用途后,想必你很想去了解 react 是如何从诸多的待处理任务中挑选出一种或者多种类型的任务去执行的。好吧,下面,我们来研究一下。

getNextLanes() 中可以说是 lane 模型中最复杂的函数,因为这个函数的实现中涉及到大量的概念。所以,在系统阐述它对 lane 的 resolve 算法之前,我们不妨简单地过下所所涉及的概念:

pendingLanes

无论是用户通过调用 setState() 方法而产生的,还是 react 内部直接产生的 update 对象,只要它意在请求走一遍界面更新流程,react 都会通过 markRootUpdated() 把这些 update 对象所在的 lane 追加到 root 的 pendingLanes 属性中,用来表示当前有这么多类型的任务正在排队。这跟去医院去看病很像。lane 被追加到 root.pendingLanes,就是相当于病人线上挂号成功;而假如一个 lane 被包含在 getNextLanes() 的返回结果中,那就是代表这病人取号成功,下一步就是到就医室里面看医生了(真正地进入界面更新流程)。

suspendedLanes

react 大概会在两种情况下挂起任务:

  • 执行任务过程中发生了 js 执行错误。这种情况下,react 会把当前的任务挂起,在相对空闲的时候再去执行一次。react 会通过 js 的try{...}catch(){...}捕获任务执行过程中出现的错误。比如,在并发渲染流程的入口函数里面就会这么干:
js 复制代码
function renderRootConcurrent(root, lanes) {
    ...
      do {
      try {
        workLoopConcurrent();
        break;
      } catch (thrownValue) {
        handleError(root, thrownValue);
      }
    } while (true);
    ...
}

采用同样的模式的还有同步渲染流程的入口函数:

js 复制代码
function renderRootSync(root, lanes) {
    ...
      do {
      try {
        workLoopSync();
        break;
      } catch (thrownValue) {
        handleError(root, thrownValue);
      }
    } while (true);
    ...
}

handleError() 函数中会把当年的任务执行结果标记为RootFatalErrored。然后,退出 work loop 之后,在接下来的后面的流程中, react 会检查任务的退出状态 exitStatus,如果是 RootFatalErrored,则会调用 markRootSuspended() 来将当前的 lanes 记录为 suspendedLanes

  • <Suspense> 组件包裹子组件的时候,react 会接受到一个 promise,当这个 promise 处于 pending 状态的时候,react 就会把 <Suspense> 组件标记为挂起的,与此同时,与此同时会通过markRootSuspended()把当前的 lane 记录到 suspendedLanes 里面来。
pingedLanes

<Suspense> 组件所关联的那个 promise 被 resolve 的时候,react 会把之前被挂起的 lane 转移到 pingedLanes 上面来,表示可以真正去渲染<Suspense> 组件里面 primary children 了。

blockedLanes

suspendedLanes 的别名。

wipLanes

当前正处于 render 阶段的所有 lanes。

entangledLanes

A lane is said to be entangled with another when it's not allowed to render in a batch that does not also include the other lane.

什么叫做「entangledLanes」呢?从官方给出的定义来看,如果两个 lane 必须放在同一个批次去执行的话,那么我们就称它们为「纠缠到一起的」lanes。这样的 lanes 是会被记录到到 root.entanglements 属性里面去(注意,处于纠缠关系的那两个 lane 都会被记录进去)。

root.entangledLanesroot.entanglements 有啥关系。root.entangledLanes 记录的是单个被纠缠的一方,要想找到另一方,react 就需要到 root.entanglements 里面去查找。

有了上面对众多概念的基本了解后,我们就可以深入阐述一下 getNextLanes() 所包含的对 lane 的 resolve 算法。

getNextLanes() 的源码比较庞大,这里就不摆出来了。

getNextLanes() 中,对 lane 进行 resolve 的算法大概可以分为三个步骤:

  1. pendingLanes 选择一个最高优先级的 lane。如果有这样的 lane 就继续;否则就返回 NoLanes,用于指示当前没有任务要执行。完整流程图如下:
graph TD A("开始") --> C["从所有的待处理任务重找出所有『非空闲类型的任务』"] C --> D{"是否存在非空闲类型的任务?"} D -->|是|E["从非空闲类型的任务中排除掉所有的『被挂起的任务』"] D -->|否|H["当前等处理的任务都是『空闲类型的任务』"] E -->F{"当前非空闲类型的任务中,是否有未被挂起的任务?"} F -->|是|G["则从「非空闲类型,同时未被挂起的」任务中取出优先级最高的 lane 作为结果返回"] F -->|否|G2{"当前待处理的任务中是否存在 「pinged」类型的任务?"} G2 -->|是|G3["则从这些「pinged」任务中取出优先级最高的 lane 作为结果返回"] G2 -->|否|G4["返回 NoLanes"] H --> J{"当前空闲类型的任务中,是否有未被挂起的任务?"} J -->|是|K["则从「空闲类型,同时未被挂起的」任务中取出优先级最高的 lane 作为结果返回"] J -->|否|K2["直接从 `root.pingedLanes` 中取出优先级最高的 lane 作为结果返回"] K2 --> END("结束") G4 --> END("结束") G3 --> END("结束") K --> END("结束") G --> END("结束")

从上面的流程图中,我们可以看出几点:

  • 挂起类型的任务只有被转换为 pinged 类型的任务才会参与得到执行;
  • pinged 类型的任务的任务优先级比空闲型任务的优先级还低;
  • 步骤一有两种结果:第一种是 resolve 出单个 lane;第二种就是空集(NoLane),表示当下没有任务需要被执行。如果是第二种结果, getNextLanes() 直接返回,不会进入步骤二;
  1. 决定出是否要中断当前的渲染阶段。 如果上一个步骤中能 resolve 出一个单一的 lane,那就是表示有任务需要被执行。react 就会尝试进入步骤二。

记住,react 只是尝试进入步骤二。只有同时满足以下条件,react 才会进入步骤二:

  • 首先当前得处于 render 阶段;
  • render 阶段没有出现了被挂起的任务;

进入步骤二后,react 会着手去决定是否需要中断当前的渲染阶段。只要满足下面条件的其中之一就不会中断当前的渲染阶段:

  • 条件 1:步骤一得到的那个 lane 跟当前正在执行的所有任务中最高优先级的那个任务相比,如果后者优先级高,react 则不会中断当前的渲染阶段;否则,会;
  • 条件 2:步骤一得到的那个 lane 是 DefaultLane,并且当前执行的任务中没有包含 transition 类型的任务,则 react 则不会中断当前的渲染阶段。言下之意就是,DefaultLane 类型的任务不能中断 transition 类型的任务,即使从单个优先级的角度看,DefaultLane 类型的任务的优先级是比 transition 类型的任务的优先级要高。

react 是如何实现不中断上一次的 render 阶段呢(即在下一次的任务执行中接着来)?那就是直接返回 wipLanes。这里面的原理就是,如果原封不动地返回上次退出任务之前保存的 wipLanes,那么在 ensureRootIsScheduled()中,最终计算出来的最高优先级是跟之前一样的,那么 scheduler 不会产生一个新任务,换句话来说就是 scheduler 的 work loop 的下一个要执行的任务还是上次的那个老任务。借助 concurrent work loop 的 全局 workInProgress指针, react 就会继续上一次未完的渲染流程。

如果,react 决定要中断上一次的渲染流程,产生一个新的调度任务的话,那么就会进入步骤三。

  1. 处理纠缠类型的 lane - 这一步可以分两种 case:一种是特殊 case,另外一种普通 case。

特殊 case : 如果 nextLanes 包含 InputContinuousLane 类型的任务,那么我们也把待处理任务中DefaultLane(如果有的话) 加入到 nextLanes 中。这种用例一般是指,在默认是同步渲染的模式下,InputContinuousLane 类型的任务要跟 DefaultLane 放在一个批次去执行;

普通 case: 首先,计算出root.entangledLanesnextLanes 的交集。然后,遍历交集中每一个 lane,从 root.entanglements 身上去查找另一侧的纠缠方 lane。最后,把这些 lane 都追加到 nextLanes 中。

小结

到这里,getNextLanes() 的 resolve 算法就讲解完毕。算法是挺复杂的,对应了很多应用场景。不过,我将它简化为三个步骤,就应该很好理解。不过值的一提的是:每一个步骤在进入下一个步骤之前都有可能提前退出。

参与基于 update 对象去计算组件状态的流程

上面讲过,生产出来的 lane 接着都会被「贴到」 update 对象上。最终,在 react 进入 render 阶段之后,在组件被渲染的时候参与到组件状态的计算中来。

在我许多的专栏文章中,我都提到了两个事实:

  • hook 函数是有两个阶段的: mount 阶段和 update 阶段;
  • 在 update 阶段中,useState() 只是 useReducer() 的一个提起内置了 basicStateReducer的一个封装而已。

基于上面两个事实,所以,我们要看的源码就是 updateReducer()函数的实现源码:

此处,我们不具体讲 react 是如何计算组件状态的逻辑,只讲述跟 lane 模型相关的那部分。

js 复制代码
function updateReducer(reducer, initialArg, init) {
    do {
        ...
        const updateLane = removeLanes(update.lane, OffscreenLane);
        const isHiddenUpdate = updateLane !== update.lane;
        const shouldSkipUpdate = isHiddenUpdate
            ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
            : !isSubsetOfLanes(renderLanes, updateLane);

        if (shouldSkipUpdate) {
            ... // case ①
        }else {
            ... // case ②
        }
    } while (update !== null && update !== first);
}

从上面摘抄下来的源码我们看到 updateReducer() 函数的实现框架 - 本质上就是遍历 baseQueue 循环单向链表上面的 update 对象,基于 baseState 来采用 reduce 算法来将链表上的 update 对象『累积』在 baseState上。最终,我们得到一个 newState。这个 newState 会成为下一个渲染周期的 baseState。然后再次遍历 baseQueue ......如此循环往复。

如果没有优先级概念的话,updateReducer() 函数的核心实现部分可以简单地实现为:

js 复制代码
function updateReducer(reducer, initialArg, init) {
    ...
    let newState = current.baseState;
    const first = baseQueue.next;
    let update = first;
    do {
        const action = update.action;
        newState = reducer(newState, action);
        update = update.next;
    } while (update !== null && update !== first);
}

也即是说,如果没有优先级的概念,所有的 update 对象都参与到 reduce 算法里面,没有漏网之鱼。也可以换句话说,update queue 上的所有 update 对象会在同一个界面流程中被 commit 到屏幕上。

当有了优先级概念。事件(update 对象如何参与到组件状态的计算)的走向就不一样了。这就是上面的注释中「case ①」和「case ②」所暗指的那样。当我们给 update 对象贴上一个 lane 来表示它的优先级的时候,那么同一个 update queue 中 update 对象的优先级就有了高低之差。而最重要的是,react 会用一个 renderLanes 来表示当前的界面更新流程是要处理具备哪些优先级的任务。

假如,当前遍历到的某个 update 对象的 lane 不是 renderLanes的子集的话(注意,这里是讲集合概念,而不是讲优先级高低的概念),那么这就代表你不在本次渲染所需要的那些 update 对象之列 - 即当前的 update 对象需要被跳过了。这个时候,shouldSkipUpdate 的值就是 true。相应的,代码的执行就进入 case ①;否则,进入 case ② - 该 update 对象会正常地参与组件状态的计算流程。

下面,简单总结一下 lane 参与组件状态计算的全流程:

  1. update 对象在创建的时候,react 会根据触发更新的场景生产出一个 lane,并贴在 update 身上;
  2. 在真正执行一次任务之前,react 会从 root.pendingLanes resolve 出一组 render lanes(一般情况下,其实只有一个 lane),用它表示当前这一次渲染周期,react 要执行的是哪些类型的任务。
  3. 在 render 阶段,组件会被渲染。组件被渲染的最核心工作之一就是「计算组件状态」。react 会遍历 update queue(baseQueue)上的所有 update 对象,只有 update 对象的 lane 标签被包含在 render lanes 里面,才会参与到本渲染周期中组件状态的计算;否则,就只能参与下一个渲染周期了。

用于标记某个优先级上是否还有任务在等待执行

  1. 在对 fibe node 进行 begin work 之前,workInProgress 的 lanes 被清空了,等待重新填充:

    js 复制代码
    function beginWork(
      current: Fiber | null,
      workInProgress: Fiber,
      renderLanes: Lanes,
    ): Fiber | null {
        ...
        workInProgress.lanes = NoLanes;
        switch (workInProgress.tag) {
            ...
        }
    }

    在对 fibe node 进行 begin work 之后,如果某些 update 对象因为优先级不高而被跳过,那么所有被跳过的 update 对象所对应的 lane 会被合并到 workInProgress.lanes 上面,代表本次渲染周期并没有处理这些 lane 。它们需要放在下一个渲染周期去处理。这一步发生在 updateReducer() 里面。

    js 复制代码
    function updateReducer(reducer, initialArg, init) {
        ...
        if (shouldSkipUpdate) {
            ...
            currentlyRenderingFiber.lanes = mergeLanes(
                currentlyRenderingFiber.lanes,
                updateLane
          );
          ...
        }
        ...
    }
  2. 此时,host root fiber 的 childLanes 属性还没有被更新。直到 react 对 host root fiber 完成了 complete work,childLanes 属性值才得到更新,它的值是所有子 fiber node 的 lanes 合并而成。这一步发生在 complete-work 阶段的 bubbleProperties(workInProgress()

  3. commitRootImpl() 里面,root.finishedWork 其实就是当前的 workInProgress fiber 树。这里拿并发渲染模式下的入口函数 performConcurrentWorkOnRoot() 举例:

    js 复制代码
    function performConcurrentWorkOnRoot(root, didTimeout) {
        ...
        if (exitStatus === RootDidNotComplete) {
            ...
        }else {
            // The render completed.
            ...
            const finishedWork = root.current.alternate;
            ...
            root.finishedWork = finishedWork;
            ...
            finishConcurrentRender(root, exitStatus, lanes);
        }
    }

    finishConcurrentRender() 函数的主流程是进入 commit 阶段。接下来的调用栈是:

    js 复制代码
    finishConcurrentRender() ->
    commitRoot() ->
    commitRootImpl()

    commitRootImpl() 是 commmit 阶段的核心函数。在它里面,react 完成了真正的 commit 动作。这个 commit 动作是由三个阶段组成:

    • before-mutation
    • mutation
    • layout

    不过本小节关注的不是这三个子阶段。而是这三个子阶段之前的一个小动作。那就是最终使用了 workInProgress fiber 树上 root 节点上 laneschildLanes 来代表了当前还没有处理的任务还有哪些:

    js 复制代码
      function commitRootImpl(
        root,
        recoverableErrors,
        transitions,
        renderPriorityLevel
      ) {
        ...
        const finishedWork = root.finishedWork;
        ...
        // Check which lanes no longer have any work scheduled on them, and mark
        // those as finished.
       // 一般情况下,)`remainingLanes`的值主要是来自于 `finishedWork.childLanes`。而 finishedWork.childLanes 的值又是通过合并它的所有的子 fiber node 的 `lanes` 而得来的
        let remainingLanes = mergeLanes(
          finishedWork.lanes,
          finishedWork.childLanes
        );
        ...
        markRootFinished(root, remainingLanes);
      }

    最后,markRootFinished() 要做的就是把 remainingLanes 记录回 root.pendingLanes 中:

    js 复制代码
      function markRootFinished(root, remainingLanes) {
        const noLongerPendingLanes = root.pendingLanes & ~remainingLanes;
        root.pendingLanes = remainingLanes;
    
        // Let's try everything again
        ...
      }

    下次开启界面更新流程之前,react 又会通过 getNextLanes()来消费root.pendingLanes,以便找出要做的任务是什么。至此,我们就看清楚 lane 是如何指示「是否还有未完成的任务」以及「还有什么类型的未完成任务」这两层语义的。

小结

react 是通过下面的流程来赋予 lane 「是否还有未完成的任务」以及「还有什么类型的未完成任务」等语义的:

  1. 在对每一个 fiber node 进行 begin work 之前把 fiber.lanes 清空。在这一步,用于指示任务存在性和类型性的 lane 信息是被丢失了;
  2. 流程继续推进。react 最终会对组件进行渲染。在这一步骤中,react 会把被跳过的 update 对象所对应的 lane 记录回fiber.lanes 属性上,至此,fiber.lanes 算是间接地完成了更新;
  3. 伴随着对 host root fiber 进行 complete work,整个 render 阶段就结束了。在对 host root fiber 进行 complete work 的时候,react 会把 host root fiber 之下的子 fiber 的 lanes 收集回来,存放在 hostRootFiber.childLanes属性上;
  4. 当界面更新流程的 commit 阶段结束后,react 会把hostRootFiber.childLaneshostRootFiber.lanes 合并起来,最终放回 root.pendingLanes 上面。至此,root.pendingLanes 的值就能完整地代表着「是否还有未完成的任务」和「还有什么类型的未完成任务」。

决定某次任务的执行是否启用 time slicing

如果通过 getNextLanes() resolve 出来的 lanes 包含阻塞型的 lane 或者过期的 lane 又或者 scheduler 告诉我们这次任务已经过期了(超过了它的 expirationTime),那么 react 就会走同步渲染模式。这个逻辑很直白地写在了 ensureRootIsScheduled() 的内部:

js 复制代码
function performConcurrentWorkOnRoot(root, didTimeout) {
    ...
     const lanes = getNextLanes(
      root,
      root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
    );
    ...
    const shouldTimeSlice =
      !includesBlockingLane(root, lanes) &&
      !includesExpiredLane(root, lanes) &&
      !didTimeout;

    let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes);
}

这里提一个题外话 - 从上面的代码中,我们可以看出,当 react 告知 scheduler 需要调度的任务函数是 performConcurrentWorkOnRoot() 的时候,并不代表这个任务的所有子任务都是采用并发渲染模式,也有可能采用同步渲染模式(往往是最后那一次子任务是采用同步渲染模式)。

除了 didTimeout 参数值是 scheduler 传递过来的,跟 lane 无关,其他的两个条件都是跟 lane 相关的。下面,我们简单地看看,什么样的 lane 是阻塞型的 lane:

js 复制代码
function includesBlockingLane(root, lanes) {
  const SyncDefaultLanes =
    InputContinuousHydrationLane |
    InputContinuousLane |
    DefaultHydrationLane |
    DefaultLane;
  return (lanes & SyncDefaultLanes) !== NoLanes;
}

上面的代码已经不言而喻了。值得一提的是,如果任务是属于 SyncLane 类型的话,那么走同步渲染是很好理解了。而我们从上面的代码可以知道:

  • InputContinuousLane
  • DefaultLane

也是走的是同步渲染模式。看来,真正走并发渲染模式的任务类型没几个 - 也许只有 transition 和 suspend 类型的任务了。

判断当前的 lanes 是否有过期的 lane,这个逻辑也是很简单:

js 复制代码
function includesExpiredLane(root, lanes) {
  // This is a separate check from includesBlockingLane because a lane can
  // expire after a render has already started.
  return (lanes & root.expiredLanes) !== NoLanes;
}

关键是 root.expiredLanes 的值是怎么来的。答案是:"每一次 react 尝试开启一个界面更新流程之前,它都会去检查一下所有的 pendingLanes,如果有过期的就把它记录在 root.expiredLanes":

js 复制代码
  function ensureRootIsScheduled(root, currentTime) {
    const existingCallbackNode = root.callbackNode;

    // Check if any lanes are being starved by other work. If so, mark them as
    // expired so we know to work on those next.
    markStarvedLanesAsExpired(root, currentTime);
    ...
  }

如果当前决定要执行的任务里面包含过期任务,那么 react 就会用同步渲染模式去执行任务。这么做的目的就是为了某些低优先级的任务因为一直被高优先级的任务打断而「饿死」。

(贴在 fiber 上,)在遍历 fiber tree 的时候决定了组件是否需要被渲染

关于这个用途,我们在上篇文章《2023 年,你是时候要掌握 react 的并发渲染了(4) - lane 模型(1)》的「lane 的生命周期 - 分发」这个小节有提到过。在这个小节中,我们重点分析了 markUpdateLaneFromFiberToRoot() 这个函数。简而言之,这个函数就是从触发更新请求的源头 fiber node 开始,一路遍历到 host root fiber,对途经的 fiber node 的 childLanes 属性值进行更新 - 把当前的 update 对象所属的 lane 合并到它自身的值之中。

做完上面这个动作后,react 就会进入真正的界面更新流程。在 render 阶段,众所周知是有两个子阶段:

  • begin-work
  • complete-work

在 begin-work 子阶段,react 有一个重大决策需要做,那就是「决定是否要跳过当前 workInProgress fiber node 的 reconciliation 流程」。跳过某个 fiber node 的 reconciliation 流程,则意味着跳过该 fiber node 所对应组件的渲染过程。从用户的角度来看,也就是说组件的渲染函数就不会被执行了。这一切都藏在 beginWork() 的函数实现里面。下面一起来看看:

js 复制代码
function beginWork(current, workInProgress, renderLanes) {'
    ...
    if (
    oldProps !== newProps ||
    hasContextChanged()
    ) {
        didReceiveUpdate = true;
    }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
          );
        }
        ...
    }
    ...
}

从上面的代码我们可以看出,需要同时满足下面的条件,react 才会「尝试」提前 bailout(注意,这里强调的是「尝试」,在这一步,还没真正 bailout),从而不对某个组件进行渲染:

  1. 组件的新旧 props 值相同(在这里,react 使用了基于引用相等性来做判断);
  2. 如果组件依赖了 context,当前该 context 值没有发生变化;
  3. 如果当前的 fiber node 并没有触发更新请求或者没有符合当前渲染优先级的更新请求。

第三点是跟 lane 模型相关的的。从这一点,我们可以得知,当前 fiber node 即使触发了更新请求,对应的组件也不一定会被渲染。为什么?因为它所包含的所有的 lane 的优先级可能都不够高(相比于本渲染周期决定要处理的 lanes - 即renderLanes)。这一点的判断逻辑藏在 checkScheduledUpdateOrContext() 函数里面:

js 复制代码
function checkScheduledUpdateOrContext(current, renderLanes) {
  // Before performing an early bailout, we must check if there are pending
  // updates or context.
  const updateLanes = current.lanes;

  if (includesSomeLane(updateLanes, renderLanes)) {
    return true;
  } // No pending update, but because context is propagated lazily, we need

  return false;
}

假如满足了上面三个条件,那么 react 就会进入 attemptEarlyBailoutIfNoScheduledUpdate() 函数里面。该函数大部分情况下最终会调用bailoutOnAlreadyFinishedWork():

js 复制代码
 function attemptEarlyBailoutIfNoScheduledUpdate(
    current,
    workInProgress,
    renderLanes
  ) {
    ...
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }

bailoutOnAlreadyFinishedWork() 中,react 会进一步检查当前 workInProgress fiber 的 childLanes 属性,看看以当前 fiber node 为根节点的子树上是否有符合条件的更新请求。如果没有的话,react 通过返回 null 来结束 begin work 阶段,提前进入当前 workInProgress fiber 的 complete work 阶段。「提前进入当前 workInProgress fiber 的 complete work 阶段」这句话意味着,react 并没有按照正常情下去对当前的 workInProgress 的子 fiber 节点进行 begin work。我们可以把这里的 bailout 理解为 react 优化 reconciliation 流程的一个手段。

综上所述,总结起来,lane 模型在当前这种用途的参与点有两个:

  1. 通过 markUpdateLaneFromFiberToRoot() 把更新请求所对应的 lane 一路从源头 fiber node 一直标记到 host root fiber;
  2. 在 begin - work 阶段,react 会做「是否要跳过当前 workInProgress fiber 节点的 reconciliation 流程」的决策。做这个决策要依据四个条件。其中有两个是跟 lane 模型相关,它们就是:
  • 判断当前的 workInProgress fiber 自身身上没有更新请求(也就是说检查它的 workInProgress.lanes 属性值);
  • 判断以当前 workInProgress fiber 为根节点的子树上的任意一个 fiber node 是否存在更新请求(也就是说检查它的 workInProgress.lanes 属性值)

总结

lane 模型在 react 内部中肯定还有很多应用场景,在这里就不一一罗列出出来了。关于 lane 模型,我一共写了两篇文章,上一篇是《2023 年,你是时候要掌握 react 的并发渲染了(4) - lane 模型(1)》。通过这两篇文章,我们明白一个事实,lane 模型之所以抽象难懂是因为它采用了位掩码的二进制来实现一个多语义系统,这就是所谓「表现力」。但也正因为 lane 模型具有多语义,它才显得弹性十足和强大无比。下面我们来总结一下它的多个语义:

  • 单个 lane 可以表达更新任务的类型;
  • 单个 lane 可以表达更新任务具有某个特定的优先级;
  • lanes 可以表达更新任务的存在性问题;
  • lanes 可以表达更新任务的批量性;
相关推荐
一颗花生米。18 分钟前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
学习使我快乐0123 分钟前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio199523 分钟前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
勿语&1 小时前
Element-UI Plus 暗黑主题切换及自定义主题色
开发语言·javascript·ui
黄尚圈圈1 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水2 小时前
简洁之道 - React Hook Form
前端
正小安4 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch6 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光6 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   6 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发