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()
里面,上面已经讲述过了); -
用它来判断是否还有待执行的任务,用法:
jsif (nextLanes === NoLanes){ // do something } 或者 if (nextLanes !== NoLanes){ // do something }
这种请用场景主要是指
performConcurrentWorkOnRoot()
里面,它在进入 concurrent work loop 之前会有一个防守:jsfunction 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()
):jsfunction 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.entangledLanes
跟 root.entanglements
有啥关系。root.entangledLanes
记录的是单个被纠缠的一方,要想找到另一方,react 就需要到 root.entanglements
里面去查找。
有了上面对众多概念的基本了解后,我们就可以深入阐述一下 getNextLanes()
所包含的对 lane 的 resolve 算法。
getNextLanes()
的源码比较庞大,这里就不摆出来了。
getNextLanes()
中,对 lane 进行 resolve 的算法大概可以分为三个步骤:
- 从
pendingLanes
选择一个最高优先级的 lane。如果有这样的 lane 就继续;否则就返回NoLanes
,用于指示当前没有任务要执行。完整流程图如下:
从上面的流程图中,我们可以看出几点:
- 挂起类型的任务只有被转换为 pinged 类型的任务才会参与得到执行;
- pinged 类型的任务的任务优先级比空闲型任务的优先级还低;
- 步骤一有两种结果:第一种是 resolve 出单个 lane;第二种就是空集(
NoLane
),表示当下没有任务需要被执行。如果是第二种结果,getNextLanes()
直接返回,不会进入步骤二;
- 决定出是否要中断当前的渲染阶段。 如果上一个步骤中能 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 决定要中断上一次的渲染流程,产生一个新的调度任务的话,那么就会进入步骤三。
- 处理纠缠类型的 lane - 这一步可以分两种 case:一种是特殊 case,另外一种普通 case。
特殊 case : 如果 nextLanes
包含 InputContinuousLane
类型的任务,那么我们也把待处理任务中DefaultLane
(如果有的话) 加入到 nextLanes
中。这种用例一般是指,在默认是同步渲染的模式下,InputContinuousLane
类型的任务要跟 DefaultLane
放在一个批次去执行;
普通 case: 首先,计算出root.entangledLanes
和 nextLanes
的交集。然后,遍历交集中每一个 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 参与组件状态计算的全流程:
- update 对象在创建的时候,react 会根据触发更新的场景生产出一个 lane,并贴在 update 身上;
- 在真正执行一次任务之前,react 会从
root.pendingLanes
resolve 出一组 render lanes(一般情况下,其实只有一个 lane),用它表示当前这一次渲染周期,react 要执行的是哪些类型的任务。 - 在 render 阶段,组件会被渲染。组件被渲染的最核心工作之一就是「计算组件状态」。react 会遍历 update queue(
baseQueue
)上的所有 update 对象,只有 update 对象的 lane 标签被包含在 render lanes 里面,才会参与到本渲染周期中组件状态的计算;否则,就只能参与下一个渲染周期了。
用于标记某个优先级上是否还有任务在等待执行
-
在对 fibe node 进行 begin work 之前,workInProgress 的 lanes 被清空了,等待重新填充:
jsfunction 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()
里面。jsfunction updateReducer(reducer, initialArg, init) { ... if (shouldSkipUpdate) { ... currentlyRenderingFiber.lanes = mergeLanes( currentlyRenderingFiber.lanes, updateLane ); ... } ... }
-
此时,host root fiber 的
childLanes
属性还没有被更新。直到 react 对 host root fiber 完成了 complete work,childLanes 属性值才得到更新,它的值是所有子 fiber node 的 lanes 合并而成。这一步发生在 complete-work 阶段的bubbleProperties(workInProgress()
。 -
在
commitRootImpl()
里面,root.finishedWork
其实就是当前的 workInProgress fiber 树。这里拿并发渲染模式下的入口函数performConcurrentWorkOnRoot()
举例:jsfunction performConcurrentWorkOnRoot(root, didTimeout) { ... if (exitStatus === RootDidNotComplete) { ... }else { // The render completed. ... const finishedWork = root.current.alternate; ... root.finishedWork = finishedWork; ... finishConcurrentRender(root, exitStatus, lanes); } }
finishConcurrentRender()
函数的主流程是进入 commit 阶段。接下来的调用栈是:jsfinishConcurrentRender() -> commitRoot() -> commitRootImpl()
commitRootImpl()
是 commmit 阶段的核心函数。在它里面,react 完成了真正的 commit 动作。这个 commit 动作是由三个阶段组成:- before-mutation
- mutation
- layout
不过本小节关注的不是这三个子阶段。而是这三个子阶段之前的一个小动作。那就是最终使用了 workInProgress fiber 树上 root 节点上
lanes
和childLanes
来代表了当前还没有处理的任务还有哪些:jsfunction 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
中:jsfunction markRootFinished(root, remainingLanes) { const noLongerPendingLanes = root.pendingLanes & ~remainingLanes; root.pendingLanes = remainingLanes; // Let's try everything again ... }
下次开启界面更新流程之前,react 又会通过
getNextLanes()
来消费root.pendingLanes
,以便找出要做的任务是什么。至此,我们就看清楚 lane 是如何指示「是否还有未完成的任务」以及「还有什么类型的未完成任务」这两层语义的。
小结
react 是通过下面的流程来赋予 lane 「是否还有未完成的任务」以及「还有什么类型的未完成任务」等语义的:
- 在对每一个 fiber node 进行 begin work 之前把
fiber.lanes
清空。在这一步,用于指示任务存在性和类型性的 lane 信息是被丢失了; - 流程继续推进。react 最终会对组件进行渲染。在这一步骤中,react 会把被跳过的 update 对象所对应的 lane 记录回
fiber.lanes
属性上,至此,fiber.lanes
算是间接地完成了更新; - 伴随着对 host root fiber 进行 complete work,整个 render 阶段就结束了。在对 host root fiber 进行 complete work 的时候,react 会把 host root fiber 之下的子 fiber 的 lanes 收集回来,存放在
hostRootFiber.childLanes
属性上; - 当界面更新流程的 commit 阶段结束后,react 会把
hostRootFiber.childLanes
和hostRootFiber.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),从而不对某个组件进行渲染:
- 组件的新旧 props 值相同(在这里,react 使用了基于引用相等性来做判断);
- 如果组件依赖了 context,当前该 context 值没有发生变化;
- 如果当前的 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 模型在当前这种用途的参与点有两个:
- 通过
markUpdateLaneFromFiberToRoot()
把更新请求所对应的 lane 一路从源头 fiber node 一直标记到 host root fiber; - 在 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
可以表达更新任务的批量性;