React 为什么使用 Lane 技术方案
React 为什么使用 Lane 技术方案React 为什么使用 Lane 模型替代 ExpirationTime 模型 - 掘金 背景(ExpirationTime 存在的问题) ExpirationTime 原理 在使用 Lane 模型之前,React 内部使用 ExpirationTime 表示任务的优先级。 调度器每次选出任务中最大的 ExpirationTime 作为执行任务的优先级 currentExecTaskTime,交给 React 进入调和阶段,即每次执行优先级最高的任务。 ExpirationTime 与批量更新 React 实现批量更新的方式很容易理解,只要任务满足 task.expirationTime >= currentExecTaskTime 即可。 在事件处理函数或生命周期函数中实现批量更新,就是通过将任务设置为相同的 ExpirationTime。如此一来,这些任务将同时满足 task.expirationTime >= currentExecTaskTime 并被执行。 ExpirationTime 与并发模式 使用 ExpirationTime 的方式是可以实现并发模式的(Concurrent Mode)。例如:当低优先级任务 taskA 还在调和阶段时,用户在 中输入内容将产生高优先级任务 taskB, taskB 负责更新 input,使其展示刚刚输入的内容。尽管 taskA 的调和阶段还没有完成,但由于 taskB.expirationTime 大于 taskA.expirationTime,所以会优先调度 taskB 进入调和阶段。这种场景下,React 使 taskA 和 taskB 同时执行,也就实现了并发模式。 存在不足(Suspense 的出现) Demo 先展示 Demo 以便直观感受使用 ExpirationTime 时遇到的问题。该 Demo 可线上访问,请戳这里。 在 Demo 中 count 值和 App: 每秒钟都会加 1。关键代码如下: js 代码解读复制代码const Sub = ({ count }) => { const [resource, setResource] = useState(undefined) // 点击按钮 3 秒后触发 Suspense 的 fallback 逻辑 const [startTransition, isPending] = useTransition({ timeoutMs: 3000 }) return (
{JSON.stringify({ count, isPending }, null, 2)} {resource === undefined ? "Initial state" : resource.read()} 在 timeout 时间范围内 Suspense 下子组件抛出 thenable 对象
在 Demo 中调试发现,触发 taskB 后,React 先尝试将 taskA 和 taskB 都执行,即在调和阶段执行 taskA 和 taskB。因为执行过程中 Sub 组件抛出 thenable 对象,所以会在提交阶段前判断当前时间是否在 timeout 时间范围内。如果在 timeout 时间范围内,则忽略本次调和阶段的执行结果,不进入提交阶段,否则才进行提交阶段,最后在提交阶段会判断是展示 children 还是展示 fallback。
期望 Demo 接下来看看在该场景下期望的效果。该 Demo 使用 Lane 模型的 React,并可线上访问,请戳这里。 上图中,在 count 为 2 时点击了 「CLICK ME」按钮,看到的现象为: isPending 立马由 false 变为 true count 值和 App: 每秒增加 1 3 秒后 Suspense 挂载 fallback,此时 App: 为 5 再过 3 秒,Suspense 挂载 children,此时 count 值和 App: 为 8 解决问题 尝试在 ExpirationTime 上修复 前面提到 ExpirationTime 引起问题场景是:「高优先级 IO 任务」阻塞了「低优先级 CPU 任务」。在该场景中,ExpirationTime 机制不能跳过高优先级的 IO 任务,导致低优先级的 CPU 任务(增加 count 值)没有执行,造成页面卡住。 ExpirationTime 机制引起该问题的更深层次原因是,它耦合了任务的优先级和批量更新。当决定了需要执行的优先级(currentExecTaskTime)时,所有 task.expirationTime >= currentExecTaskTime 的任务都将被执行。 显然解决上述问题就需要支持先执行低优先级任务,再执行高优先级任务,在 ExpirationTime 机制下不可行 使用 Lane 模型 React 最后选择使用 Lane 来表达任务的优先级,Lane 通过二进制位来表示。优先级最高的 SyncLane 为 1,其次为 2、4、8 等等,所有 Lane 的定义可参考源码。 通过 Lanes 表达批量更新。Lanes 是一个整数,该整数所有二进制位为 1 对应的优先级任务都将被执行。例如 Lanes 为 17 时,表示将批量更新 SyncLane(值为 1)和 DefaultLane(值为 16)的任务。 Lane 模式下的 Demo 前面也提到,Lane 模式下的 Demo 运行结果符合预期。该 Demo 可线上访问,请戳这里。 点击「CLICK ME」按钮后,执行 setResource(createResource(6000)) 将生成优先级为 8192 的任务,该优先级对应 TransitionShortLanes。执行 setS((x) => x + 1) 将生成优先级为 512 的任务,该优先级对应 DefaultLanes。 每次定时器触发时,会先行优先级为 512 的任务,引起 count 值和 App: 增加 1。随后执行优先级为 8192 的任务,但由于该任务是未完成的 IO 任务,所以不会进入提交阶段。 事实上使用 Lane 模型后,IO 任务的优先级始终低于 CPU 任务(增加 count 值)的优先级,所以该机制并没有利用“先执行低优先级任务,再执行高优先级”的方式来解决问题。 ExpirationTime 就达不到这种效果,理论上可以每次产生新的 CPU 任务时就给所有 IO 任务的 ExpirationTime 做减法,来降低 IO 任务的优先级。但这种方式时间复杂度高,容易导致数值溢出,显然是不可行的。 使用 Lane 的不足
- 饥饿任务 使用 Lane 而不是 ExpirationTime 可能会导致饥饿任务(starvation),目前 React 通过在调度器中为任务实现 timeout 机制。
参考 Initial Lanes implementation 中 「Stuff I intentionally omitted from this initial PR」 一节。
- 可读性差 调试时要将 Lanes 转换为对应的优先级是在是太难了 😭。



