React18 真的 all in concurrent 了吗?你可能想错了。
最近 react18 正式版也发布了,大家都说 React18 默认已经开启 concurrent 模式。结果今天测试了半天发现都使用的是 Sync 模式,难道是我的使用方式有问题?遇事不决,打开源码。不看不知道,一看吓一跳,原来并不是我想的那样,React18 并没有 all in concurrent。
大家都知道,在 react 17 中,concurrent 以及 sync 模式是由两个不同的 API 来控制:
ReactDOM.render // sync 模式
ReactDOM.createRoot(document.getElementById('root')).render() // concurrent 模式
第二种方式在 react18 以及成为默认的创建方式。但是请注意,react 17 的 concurrent 是满血的 concurrent 模式,只要不是 SyncLane 优先级的 task 都可以启用时间切片。例如典型例子是在 setTimeout 中创建的任务可以被 onClick 中的任务打断。
但是在 react 18 中,只有在 startTransition 中创建的任务才开启时间切片!!!
也就是说,如果你的任务没有包一层 startTransition,那么这个任务的协调过程不能被打断。这和 react17 有很大的区别!
源码环节
当然不能少了 show me the code 的环节。我们知道在 react 中,sync 任务以及 concurrent 会通过两个不同函数来执行,分别是 performConcurrentWorkOnRoot 以及 performSyncWorkOnRoot。而 performConcurrentWorkOnRoot 最终又会调用:
let exitStatus = renderRootConcurrent(root, lanes);
上面这段是 v17 的代码,但是这段代码在 v18 中摇身一变,多了几个判断条件:
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice // 只有在 shouldTimeSlice = true 才是真正的 renderRootConcurrent
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
那么这个 shouldTimeSlice 第一个条件是 includesBlockingLane 也就是当然任务的 lane 不包含在BlockingLane 中。那么哪些 lanes 不包含在 BlockingLane 呢?直接看函数 includesBlockingLane:
export function includesBlockingLane(root: FiberRoot, lanes: Lanes) {
// 注意这里默认是不会命中的,很多博客都说这里是默认开启,实际是不是的。并且这一个
// 判断在 react 打包后就不会存在了。
if (
allowConcurrentByDefault &&
(root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode
) {
// Concurrent updates by default always use time slicing.
return false;
}
// 如果当前任务 lane 在这几个 lanes 中,则返回 true,那么 shouldTimeSlice 就为 false,则不会开启时间切片
const SyncDefaultLanes =
InputContinuousHydrationLane |
InputContinuousLane |
DefaultHydrationLane |
DefaultLane;
return (lanes & SyncDefaultLanes) !== NoLanes;
}
可以看到如果当然任务的 lane 在 InputContinuousHydrationLane, InputContinuousLane, DefaultHydrationLane, DefaultLane 中就不会开启时间切片。那么整个优先级除了这几个 lanes 就只剩下 SyncLane 和 TransactionLane 了。SyncLane 就不用说了,肯定不会开启时间切片(因为压根就不会走到 performConcurrentWorkOnRoot 这个函数来)。也就剩下只剩 TransitionLane了,也就是使用 React.startTransition 创建的任务。
实际上在官方的文档里面也提到了 react-v18:
However, Concurrent React is more important than a typical implementation detail — it’s a foundational update to React’s core rendering model. So while it’s not super important to know how concurrency works, it may be worth knowing what it is at a high level. A key property of Concurrent React is that rendering is interruptible. When you first upgrade to React 18, before adding any concurrent features, updates are rendered the same as in previous versions of React — in a single, uninterrupted, synchronous transaction. With synchronous rendering, once an update starts rendering, nothing can interrupt it until the user can see the result on screen.
开启 concurrent 也和以前(Sync)一样,不可打断直到变更在屏幕上被看到。
总结
之前一直以为 react18 已经全面开启,而且很多博客也是这样说的。但是测试一番才发觉并非如此。如果希望你的任务能够使用时间切片,那么请使用:
React.startTransition(() => {setState(xxx)})