React 并发渲染(Concurrent Rendering)是 React 18 引入的一项重要特性,旨在提高应用的响应性和用户体验。它允许 React 在渲染过程中暂停、中断和恢复工作,从而优先处理用户交互等高优先级任务,避免长时间的渲染阻塞导致页面卡顿。 [1][2]
为什么需要并发渲染?
在 React 16 之前的版本中,更新过程是同步且不可中断的。这意味着一旦 React 开始渲染组件树,它会一气呵成地完成所有工作,直到更新真实 DOM。 [3][4] 如果组件树非常庞大,或者更新操作涉及大量计算,JavaScript 主线程会被长时间占用,导致浏览器无法及时响应用户输入(如点击、输入)或动画,从而出现卡顿现象,影响用户体验。 [1][2]
并发渲染的出现正是为了解决这个问题。它将渲染过程分解成多个可中断的小任务,并引入了优先级调度机制,使得 React 能够更灵活地利用浏览器的空闲时间,优先处理紧急任务。 [1][3]
并发渲染的核心原理
React 并发渲染的实现依赖于以下几个核心概念:
-
- 可中断性与可恢复性: Fiber 是 React 16 引入的一种全新的数据结构,它将组件树的每个节点抽象为一个 Fiber 节点。与传统的 VDOM 树不同,Fiber 节点之间通过
child
、sibling
、return
等指针相互连接,形成一个链表结构。 [1][2] 这种链表结构使得 React 能够将渲染工作分解成一个个独立的工作单元(unit of work),每个工作单元对应一个 Fiber 节点。 [3][4] 这样,React 可以在处理完一个 Fiber 节点后,将控制权交还给浏览器,让浏览器有机会处理其他任务,然后在下一个空闲时间恢复之前中断的工作。 [3][5] - 双缓冲(Double Buffering): React 维护两棵 Fiber 树:
current
树(当前屏幕上渲染的 UI 对应的 Fiber 树)和workInProgress
树(正在构建的、代表下一次更新的 Fiber 树)。 [6] 当新的更新触发时,React 会在workInProgress
树上进行计算和构建。 [6][7] 一旦workInProgress
树构建完成,并且所有更新都准备就绪,React 会通过一个简单的指针切换,将workInProgress
树变为current
树,从而一次性地将所有更新呈现在屏幕上。 [6] 这种机制保证了 UI 更新的原子性,避免了中间状态的显示。 [6]
- 可中断性与可恢复性: Fiber 是 React 16 引入的一种全新的数据结构,它将组件树的每个节点抽象为一个 Fiber 节点。与传统的 VDOM 树不同,Fiber 节点之间通过
-
工作循环与阶段(Work Loop & Phases) [1][2]
React 的渲染过程分为两个主要阶段:
- 渲染/协调阶段 (Render / Reconciliation Phase): 这个阶段是可中断的。 [1][2] React 会遍历 Fiber 树,执行组件的 render 方法,对比新旧 Fiber 节点(Diff 算法),计算出需要进行的更新(如增、删、改),并在这个阶段为 Fiber 节点打上对应的副作用标记(flags)。 [1][2] 这个阶段不会操作真实 DOM。 [8]
- 提交阶段 (Commit Phase): 这个阶段是同步且不可中断的。 [1][2] 在渲染阶段完成后,React 会进入提交阶段,根据渲染阶段打上的副作用标记,一次性地将所有更新应用到真实 DOM 上。 [1][6] 这个阶段还包括执行生命周期方法(如
componentDidMount
/Update
)、useLayoutEffect
回调、更新ref
等操作。 [1][6]
-
优先级调度 (Priority Scheduling) - Lane 模型 [1][9]
React 引入了优先级机制来管理不同更新的重要性。
-
Lane 模型: React 内部使用 Lane 模型来表示更新的优先级。 [1][9] 每个更新会被分配一个或多个 Lane,Lane 是一种位掩码(bitmask),通过位运算进行优先级的合并和判断。 [9][10] 优先级越高的 Lane,其对应的更新会越早被处理。 [11][12]
-
优先级分类: React 将更新分为不同的优先级,例如:
-
Scheduler (调度器): React 内部使用一个独立的
scheduler
包来管理任务的调度。 [1][2]scheduler
模块类似于操作系统的任务调度器,它根据任务的优先级,利用requestAnimationFrame
和MessageChannel
等浏览器 API,在浏览器空闲时执行 React 的渲染任务。 [5][13] 如果当前任务执行时间过长或有更高优先级的任务到来,scheduler
会暂停当前任务,将控制权交还给浏览器,并在适当的时机恢复。 [5][13]
-
如何实现并发渲染?
并发渲染的实现体现在 React 内部的调度逻辑和提供给开发者的 API 上。
内部实现概览:
- 更新触发: 当
setState
或其他更新函数被调用时,React 会为该更新分配一个优先级(Lane)。 [1][2] - 调度任务: React 将更新任务提交给
scheduler
。scheduler
会根据任务的优先级将其放入优先级队列中。 [5][13] - 时间切片 (Time Slicing):
scheduler
在浏览器每一帧的空闲时间(通常是 5ms)内执行优先级最高的任务。 [14] 在执行任务时,React 会不断检查是否还有剩余时间(通过shouldYield
方法)或是否有更高优先级的任务。 [14][15] - 暂停与恢复: 如果时间片用完,或者有更高优先级的任务需要处理,当前正在进行的渲染任务会被暂停,并将控制权交还给浏览器。 [5][13] 浏览器可以利用这段时间进行重绘或处理用户输入。当浏览器再次空闲时,
scheduler
会恢复之前暂停的任务,从中断的地方继续执行。 [5][13] - 提交: 一旦渲染阶段的所有工作(包括暂停和恢复)都完成,React 会进入同步的提交阶段,将更新一次性应用到真实 DOM。 [1][6]
开发者 API:
React 18 提供了 startTransition
和 useDeferredValue
等 API,让开发者可以标记哪些更新是"过渡更新",从而利用并发渲染的优势。 [1][2]
-
startTransition
[1][2]
startTransition
函数用于将一个状态更新标记为"过渡更新"(非紧急更新)。 [1][2] 被startTransition
包裹的更新优先级较低,React 会在后台渲染这部分 UI,而不会阻塞用户交互。 [7][16]代码示例:
jsximport React, { useState, startTransition } from 'react'; function SearchPage() { const [inputValue, setInputValue] = useState(''); const [searchQuery, setSearchQuery] = useState(''); const [isPending, setIsPending] = useState(false); // useTransition 才能获取 isPending const handleChange = (e) => { // 紧急更新:立即更新输入框的值 setInputValue(e.target.value); // 过渡更新:将搜索结果的更新标记为非紧急 // startTransition 不直接提供 isPending 状态,这里仅为示意 // 如果需要 isPending,通常会使用 useTransition // setIsPending(true); // 仅为示意,实际 startTransition 不提供此功能 startTransition(() => { setSearchQuery(e.target.value); // setIsPending(false); // 仅为示意 }); }; return ( <div> <input type="text" value={inputValue} onChange={handleChange} placeholder="输入搜索内容" /> {/* {isPending && <p>正在搜索...</p>} */} {/* useTransition 才能获取 isPending */} <SearchResults query={searchQuery} /> </div> ); } // 假设 SearchResults 是一个会进行耗时计算或数据获取的组件 function SearchResults({ query }) { const items = React.useMemo(() => { // 模拟耗时计算 const startTime = performance.now(); while (performance.now() - startTime < 100) { // 模拟阻塞 } return Array.from({ length: 500 }).map((_, i) => `${query} Item ${i}`); }, [query]); return ( <ul> {items.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> ); } export default SearchPage;
原理: 当
startTransition
被调用时,它会改变当前上下文的优先级。 [17] 在startTransition
回调函数内部触发的所有状态更新,都会被标记为低优先级的"过渡更新"。 [1][2] React 的调度器会优先处理高优先级的更新(如setInputValue
),而将低优先级的setSearchQuery
更新推迟到浏览器有空闲时间时再进行渲染,并且这个渲染过程是可中断的。 [1][2] -
useTransition
[1][2]
useTransition
是一个 Hook,它返回一个包含isPending
状态和startTransition
函数的数组。 [18][19]isPending
可以用来在过渡更新进行时显示加载指示器。 [18][19]代码示例:
jsximport React, { useState, useTransition } from 'react'; function SearchPageWithTransition() { const [inputValue, setInputValue] = useState(''); const [searchQuery, setSearchQuery] = useState(''); const [isPending, startTransition] = useTransition(); // 使用 useTransition const handleChange = (e) => { setInputValue(e.target.value); // 紧急更新 startTransition(() => { setSearchQuery(e.target.value); // 过渡更新 }); }; return ( <div> <input type="text" value={inputValue} onChange={handleChange} placeholder="输入搜索内容" /> {isPending && <p>正在搜索...</p>} {/* 显示加载状态 */} <SearchResults query={searchQuery} /> </div> ); } // SearchResults 组件同上 function SearchResults({ query }) { const items = React.useMemo(() => { const startTime = performance.now(); while (performance.now() - startTime < 100) { // 模拟阻塞 } return Array.from({ length: 500 }).map((_, i) => `${query} Item ${i}`); }, [query]); return ( <ul> {items.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> ); } export default SearchPageWithTransition;
-
useDeferredValue
[1][2]
useDeferredValue
Hook 允许你延迟更新 UI 的某个部分的值。 [1][2] 它返回一个"延迟"版本的值,这个延迟值会"滞后于"最新的值。 [20] 当原始值发生变化时,useDeferredValue
会立即返回旧值,同时在后台尝试使用新值进行渲染。 [20] 这种后台渲染是可中断的。 [20]代码示例:
jsximport React, { useState, useDeferredValue } from 'react'; function SearchPageWithDeferredValue() { const [inputValue, setInputValue] = useState(''); // deferredQuery 是 inputValue 的延迟版本 const deferredQuery = useDeferredValue(inputValue); // [16, 24, 34] const handleChange = (e) => { setInputValue(e.target.value); // 立即更新输入框 }; return ( <div> <input type="text" value={inputValue} onChange={handleChange} placeholder="输入搜索内容" /> {/* SearchResults 使用延迟的值,它的更新会是非阻塞的 */} <SearchResults query={deferredQuery} /> </div> ); } // SearchResults 组件同上 // 注意:为了 useDeferredValue 的优化效果, // SearchResults 组件通常需要被 React.memo 包裹, // 以便在 deferredQuery 没有变化时跳过不必要的渲染。 [16] const SearchResults = React.memo(({ query }) => { const items = React.useMemo(() => { const startTime = performance.now(); while (performance.now() - startTime < 100) { // 模拟阻塞 } return Array.from({ length: 500 }).map((_, i) => `${query} Item ${i}`); }, [query]); return ( <ul> {items.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> ); }); export default SearchPageWithDeferredValue;
原理:
useDeferredValue
的内部实现与useTransition
类似,它也是通过标记更新为过渡任务来实现的。 [18][21] 当inputValue
改变时,setInputValue
会立即更新,而deferredQuery
暂时保持旧值。React 会在后台启动一个低优先级的渲染,使用新的inputValue
来计算deferredQuery
的新值,并渲染SearchResults
。 [20] 这个后台渲染是可中断的,如果用户继续输入,新的高优先级更新会打断当前的后台渲染,并从最新的值重新开始。 [20] 这样,用户输入框始终保持响应,而耗时渲染的SearchResults
则在后台悄悄更新。 [20]
总结来说,React 并发渲染通过 Fiber 架构、优先级调度和时间切片等机制,将耗时的渲染工作拆分成小块,并在浏览器空闲时或以低优先级执行,从而确保了用户界面的流畅性和响应性。 startTransition
、useTransition
和 useDeferredValue
等 API 则是开发者利用这些底层能力,优化用户体验的利器。
好文:
- 彻底搞懂React 18 并发机制的原理 - 稀土掘金
- 彻底搞懂React 18 并发机制的原理 - 腾讯云
- 深入理解React 的Fiber 架构 - 稀土掘金
- 面试官:说说对Fiber架构的理解?解决了什么问题? | web前端面试 - Vue3
- React调度系统- Scheduler - 稀土掘金
- 【从0实现React18】 (六) 完成commit提交流程并初步实现react-dom包,完成首屏渲染测试
- React 18的并发渲染:颠覆传统的性能飞跃- 个人文章 - SegmentFault 思否
- React 中的并发渲染 - IoT技术专栏
- 干货满满,React设计原理(三):藏在源码里的排位赛,Lane模型
- 优先级管理 - 图解React
- React源码解析之优先级Lane模型上 - 稀土掘金
- 前端宝典之六:React源码解析之lane模型 - CSDN博客
- React 的调度系统Scheduler 原创 - CSDN博客
- Scheduler的原理与实现 - React技术揭秘
- React 调度原理(scheduler)
- startTransition -- React 中文文档
- 给女朋友讲React18新特性:startTransition-51CTO.COM
- V18 - Transition | 前端那些事儿
- 一文读懂React的Transition实现原理 - 稀土掘金
- useDeferredValue -- React 中文文档
- 第37节------useDeferredValue+useTransition 原创 - CSDN博客