React 18在2022年3月正式发布,最大的变化就是引入了并发特性(Concurrent Features)。
很多人用了React 18,但对并发特性的理解还停留在"用了新API"的层面。比如用了useTransition、useDeferredValue,但不知道它们为什么能让页面更流畅。
本文深入剖析React 18并发特性的底层原理,包括Fiber架构、时间切片、优先级调度、自动批处理、Suspense、Transition等。帮你从底层理解React 18的并发机制,而不只是会用API。
一、React 18之前的问题
要理解React 18的并发特性,先要知道React 18之前有什么问题。
1. 同步渲染,无法中断
React 18之前,渲染是同步的、不可中断的。一旦开始渲染,就必须渲染完,中间不能停。
如果组件树很大,渲染一次需要几百毫秒,这期间浏览器主线程被占用,用户的输入、动画都会卡顿。
比如,用户在输入框打字,同时有一个大列表在渲染。因为渲染是同步的,大列表的渲染会阻塞用户输入,导致打字卡顿。
2. 所有更新优先级相同
React 18之前,所有的更新优先级都是一样的。用户输入和后台数据更新,优先级一样,都会触发完整的重新渲染。
但实际上,用户输入的优先级应该更高,因为它直接影响用户体验。后台数据更新可以慢一点,不影响用户体验。
3. 批处理有限
React 18之前,只有在React事件处理函数中的多个setState才会被批处理。在setTimeout、Promise回调、原生事件中,多个setState不会被批处理,会触发多次渲染。
这导致不必要的渲染,影响性能。
二、Fiber架构:并发的基础
React 16引入了Fiber架构,为并发特性打下了基础。React 18的并发特性,就是在Fiber架构上实现的。
1. 什么是Fiber
Fiber是React的新的协调引擎。它把渲染任务拆分成一个个小的单元(Fiber节点),每个单元可以独立执行、暂停、恢复。
在React 15及之前,渲染是递归的,一旦开始就不能中断。Fiber把递归改成了循环,可以在每个Fiber节点之后检查是否需要中断,把控制权交还给浏览器。
2. Fiber的双缓存机制
Fiber用了双缓存机制:current树和workInProgress树。
- current树:当前屏幕上显示的树
- workInProgress树:正在构建的新树
渲染的时候,在workInProgress树上构建新的UI,构建完成后,把current指针指向workInProgress树,完成切换。
这样,渲染过程中用户看到的还是旧的UI,不会出现中间状态。
3. Fiber的副作用列表
每个Fiber节点都有一个effectTag,标记这个节点需要做什么操作(比如插入、更新、删除)。
渲染完成后,所有有副作用的Fiber节点会被收集到一个副作用列表里,然后统一执行。
这样,DOM操作是批量的,不会频繁触发重排重绘。
三、时间切片:让渲染可中断
时间切片(Time Slicing)是并发特性的核心。
1. 原理
时间切片的原理是:把渲染任务拆分成小的片段,每个片段执行一小段时间(默认5ms),然后检查是否有更高优先级的任务(比如用户输入)。如果有,就暂停当前渲染,先处理高优先级任务;如果没有,就继续渲染。
这样,浏览器主线程不会被长时间占用,用户输入和动画就能及时响应。
2. 实现方式
时间切片是通过MessageChannel实现的。React在每个时间片结束后,用MessageChannel.postMessage()把下一个时间片放到宏任务队列里。这样,浏览器就能在两个时间片之间处理用户输入、动画等任务。
为什么不用setTimeout?因为setTimeout有最小延迟(4ms),而且嵌套层级深了之后延迟会更大。MessageChannel没有这个问题。
3. 可中断和不可中断
不是所有渲染都能中断。React的渲染分为两个阶段:
- render阶段:构建Fiber树,可中断
- commit阶段:提交DOM更新,不可中断
render阶段可以中断,因为它只是在内存里构建树,没有修改DOM。commit阶段不能中断,因为它在修改DOM,中断会导致UI不一致。
四、优先级调度:让重要的事先做
并发特性的另一个核心是优先级调度。
1. 优先级分类
React 18把更新分为不同的优先级:
- Immediate:同步执行,不可中断(比如受控输入)
- UserBlocking:用户阻塞型,比如点击、输入,需要快速响应
- Normal:普通优先级,比如数据更新、网络请求回来后的更新
- Low:低优先级,比如通知、提醒
- Idle:空闲优先级,比如埋点、分析
不同优先级的任务,调度顺序不同。高优先级的任务可以打断低优先级的任务。
2. 调度器(Scheduler)
优先级调度是由Scheduler库实现的。Scheduler是React独立的调度库,负责管理任务的优先级和执行顺序。
Scheduler用小顶堆来管理任务,优先级最高的任务先执行。每个任务有一个过期时间,过期了就必须立即执行。
3. 优先级插队
当一个低优先级的渲染正在执行时,如果来了一个高优先级的更新,React会:
- 暂停当前的低优先级渲染
- 开始高优先级的渲染
- 高优先级渲染完成后,重新开始低优先级的渲染(不是继续,而是从头开始)
注意,低优先级的渲染是从头开始,不是继续。因为高优先级的更新可能改变了状态,之前的渲染结果可能已经无效了。
五、自动批处理:减少不必要的渲染
React 18的另一个重要变化是自动批处理(Automatic Batching)。
1. React 18之前的批处理
React 18之前,只有在React事件处理函数中的多个setState才会被批处理。
function handleClick() {
setCount(c => c + 1)
setFlag(f => !f)
// 这两个setState会被批处理,只触发一次渲染
}但在setTimeout、Promise回调、原生事件中,多个setState不会被批处理:
setTimeout(() => {
setCount(c => c + 1)
setFlag(f => !f)
// React 18之前:触发两次渲染
// React 18之后:只触发一次渲染
}, 1000)2. React 18的自动批处理
React 18之后,所有的更新都会被自动批处理,不管是在什么上下文中。
这是因为React 18用了并发渲染,所有的更新都会被加入队列,然后在同一个渲染周期里统一处理。
3. 如何退出批处理
如果你不想批处理,想让某个setState立即触发渲染,可以用flushSync:
import { flushSync } from 'react-dom'
function handleClick() {
flushSync(() => {
setCount(c => c + 1)
})
// 这里count已经更新了,DOM也更新了
setFlag(f => !f)
}但flushSync会影响性能,不建议频繁使用。
六、Transition:区分紧急和非紧急更新
Transition是React 18的新API,用来区分紧急更新和非紧急更新。
1. 为什么需要Transition
有些更新是紧急的,比如用户输入、点击按钮,需要立即响应。
有些更新不是紧急的,比如搜索结果过滤、路由切换、列表渲染,可以慢一点。
如果把这些更新都当成紧急的,就会导致用户输入卡顿。Transition让我们可以把非紧急的更新标记为Transition,让它们在后台执行,不阻塞用户输入。
2. useTransition
useTransition返回一个数组:[isPending, startTransition]。
import { useTransition } from 'react'
function SearchResults() {
const [query, setQuery] = useState('')
const [isPending, startTransition] = useTransition()
function handleChange(e) {
// 紧急更新:立即更新输入框
setQuery(e.target.value)
// 非紧急更新:在后台渲染搜索结果
startTransition(() => {
setSearchQuery(e.target.value)
})
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <div>加载中...</div>}
<Results query={searchQuery} />
</div>
)
}startTransition里的更新,会被标记为低优先级,可以被高优先级的更新打断。
3. isPending的作用
isPending表示Transition是否正在执行。可以用它来显示加载状态,告诉用户正在处理。
注意,isPending不是说数据在加载,而是说React正在后台渲染。渲染完成后,isPending会变成false。
七、useDeferredValue:延迟更新某个值
useDeferredValue是另一个新API,用来延迟更新某个值。
1. 原理
useDeferredValue接收一个值,返回一个延迟更新的副本。
当原始值变化时,延迟值不会立即更新,而是等当前紧急更新处理完之后,再更新。
import { useDeferredValue } from 'react'
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query)
// deferredQuery会延迟更新
return <Results query={deferredQuery} />
}2. 和useTransition的区别
- useTransition:由你控制哪些更新是Transition
- useDeferredValue:由React控制某个值的更新时机
如果你能控制更新的代码,用useTransition。如果你不能控制(比如值是从props传进来的),用useDeferredValue。
八、Suspense:声明式的加载状态
Suspense在React 16就引入了,但在React 18中,Suspense和并发渲染结合,变得更强大。
1. 基本用法
Suspense让你可以声明式地处理加载状态:
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>如果SomeComponent在渲染过程中"挂起"(抛出一个Promise),React会显示fallback。等Promise resolve后,再渲染SomeComponent。
2. Suspense和并发渲染
在React 18中,Suspense和并发渲染结合,可以实现更流畅的体验。
比如,路由切换时,新页面还在加载,React可以保持旧页面的交互,同时在后台加载新页面。加载完成后,再切换到新页面。
这就是所谓的"选择性水合"(Selective Hydration)和"流式服务端渲染"(Streaming SSR)。
3. SuspenseList
SuspenseList是一个新组件,用来控制多个Suspense的显示顺序:
<SuspenseList revealOrder="forwards">
<Suspense fallback={<Loading />}><ComponentA /></Suspense>
<Suspense fallback={<Loading />}><ComponentB /></Suspense>
<Suspense fallback={<Loading />}><ComponentC /></Suspense>
</SuspenseList>revealOrder="forwards"表示按顺序显示,A加载完了再显示B,B加载完了再显示C。这样可以避免页面内容跳动。
九、并发渲染的注意事项
使用React 18的并发特性,有一些注意事项。
1. 渲染函数必须是纯函数
因为渲染可以被中断和重新开始,所以渲染函数(函数组件的函数体)必须是纯函数,不能有副作用。
如果渲染函数有副作用(比如修改全局变量、直接操作DOM),中断后重新执行,可能会导致不可预期的结果。
副作用应该放在useEffect里。
2. 不要在渲染中读取可变数据
渲染中不要读取可变的全局数据(比如window.xxx、模块级变量)。因为渲染可能被中断,重新开始时数据可能已经变了,导致渲染结果不一致。
应该用state和props。
3. useId生成稳定的ID
并发渲染中,组件可能被多次渲染,生成的ID可能不一致。React 18提供了useId hook,用来生成稳定的ID:
function MyComponent() {
const id = useId()
return <div id={id}>...</div>
}useId生成的ID,在多次渲染中是稳定的,而且在服务端渲染和客户端水合中也是一致的。
4. 严格模式(Strict Mode)
React 18的严格模式,会在开发环境中故意双重调用渲染函数、effect、等,来帮助发现不纯的渲染。
这是为了让你的组件适应并发渲染。如果严格模式下出了问题,说明你的组件可能有副作用,需要修复。
十、并发特性的实际效果
说了这么多原理,并发特性能带来什么实际效果?
1. 更流畅的用户输入
用户输入时,大列表的渲染不会阻塞输入。打字更流畅,不会卡顿。
2. 更快的响应速度
高优先级的更新(比如点击、输入)能立即响应,低优先级的更新(比如数据更新)在后台执行。
3. 更少的不必要渲染
自动批处理减少了渲染次数,提高了性能。
4. 更好的加载体验
Suspense和Transition让加载状态更可控,用户体验更好。
十一、写在最后
React 18的并发特性,是React自Fiber架构以来最大的变化。
它让React从同步渲染走向了并发渲染,能够更好地应对复杂的应用场景。但并发特性也带来了一些新的概念和注意事项,需要我们深入理解。
本文从Fiber架构、时间切片、优先级调度、自动批处理、Transition、Suspense等方面,剖析了React 18并发特性的底层原理。希望能帮你更好地理解和使用React 18。
2022年了,React 18已经正式发布。如果你还在用React 17,建议升级到React 18,体验并发特性带来的性能提升。
最后,用一句话总结:"React 18的并发特性,本质上是让渲染可中断、可调度、可优先级化,从而让用户体验更流畅。"
祝大家的React应用都能又快又流畅。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录