原文链接:A deep dive into React Fiber - LogRocket Blog
编者注:本文由 David Omotayo 于 2024 年 10 月 22 日更新,包含自 React v16 以来 React Fiber 中的关键改进的更新,例如并发性、自动批处理以及新的 Hooks,如 useTransition
、useSyncExternalStore
和 useInsertionEffect
。
有没有想过,当你调用 ReactDOM.render(<App />, document.getElementById('root'))
时会发生什么?
我们知道,ReactDOM 会在后台构建 DOM 树,并将应用程序渲染到屏幕上。但 React 究竟是如何构建 DOM 树的呢?当应用程序的状态发生变化时,它又是如何更新 DOM 树的呢?
在本文中,我们将了解什么是 React Fiber,以及 React 在 v15.0.0 之前是如何构建 DOM 树的,该模型的缺陷,以及从 React v16.0.0 到当前版本的新模型是如何解决这些问题的。
本文将涵盖一系列纯粹是内部实现细节的概念,对于使用 React 进行实际的前端开发而言,这些概念并不是绝对要知道的。
什么是 React Fiber?
React Fiber 是一项内部引擎改进,旨在使 React 运行速度更快、更智能。Fiber 协调器已成为 React 16 及更高版本的默认协调器,它完全重写了 React 的协调算法,解决了 React 中一些长期存在的问题。
React Fiber 是一项内部引擎改进,旨在使 React 运行速度更快、更智能。Fiber 协调器已成为 React 16 及更高版本的默认协调器,它完全重写了 React 的协调算法,解决了 React 中一些长期存在的问题。
由于 Fiber 是异步的,React 可以:
-
在新的更新到来时暂停、恢复和重新启动组件的渲染工作;
-
重用之前完成的工作,甚至在不需要时中止;
-
将工作拆分成多个块,并根据重要性确定任务的优先级;
这项改进使 React 摆脱了同步堆栈协调器的限制。例如,以前,您可以添加或删除项目,但它必须一直工作到堆栈为空,并且任务不能被中断。
React 的堆栈协调器
让我们从我们熟悉的 ReactDOM.render(<App />, document.getElementById('root'))
开始。
ReactDOM
模块将 <App/>
传递给协调器 (reconciler),但这里有两个问题:
<App />
指的是什么?- 什么是协调器 (reconciler)?
让我们来解答这两个问题。
<App />
是什么?
<App />
是一个 React 元素,"元素描述了树"。[根据 React 博客](React Components, Elements, and Instances -- React Blog),"元素是描述组件实例或 DOM 节点及其所需属性的普通对象。"
换句话说,元素并非实际的 DOM 节点或组件实例;它们是一种向 React 描述它们是什么类型的元素、它们拥有哪些属性以及它们的子元素是谁的方式。
这就是 React 的真正威力所在:React 自身抽象出了构建、渲染和管理实际 DOM 树生命周期的复杂部分,从而有效地简化了开发者的工作。
为了理解这究竟意味着什么,让我们来看看一种使用面向对象概念的传统方法。
React 中的面向对象编程
在典型的面向对象编程世界中,开发者必须实例化并管理每个 DOM 元素的生命周期。例如,如果您要创建一个简单的表单和一个提交按钮,状态管理仍然需要开发者付出一些努力。
假设 Button 组件有一个 isSubmitted 状态变量。Button 组件的生命周期类似于下面的流程图,其中每个状态都必须由应用管理:

随着状态变量数量的增加,流程图的大小和代码行数会呈指数级增长。
因此,React 提供了元素来解决这个问题;在 React 中,有两种类型的元素:DOM 元素和组件元素。
DOM 元素是一个字符串元素;例如,<button class="okButton"> OK </button>
。
组件元素是一个类或函数,例如,<Button className="okButton"> OK </Button>
,其中 <Button>
可以是类或函数组件。这些是我们常用的典型 React 组件。
重要的是要理解这两种类型都是简单对象。它们仅仅是对必须在屏幕上渲染的内容的描述,并且在创建和实例化它们时不会触发渲染。
什么是 React 协调?
React 协调使得 React 更容易解析和遍历元素以构建 DOM 树。实际的渲染在遍历完成后进行。
当 React 遇到类或函数组件时,它会根据该元素的 props 询问该元素要渲染什么元素。
例如,如果 <App>
组件渲染了以下内容,那么 React 会根据 <Form>
和 <Button>
组件对应的 props 询问它们要渲染什么:
jsx
<Form>
<Button>
Submit
</Button>
</Form>
因此,如果 Form
组件是一个如下所示的函数组件,React 将调用 render()
来知道它渲染什么,并看到它渲染了一个带有子元素的 <div>
jsx
const Form = (props) => {
return(
<div className="form">
{props.form}
</div>
)
}
React 会重复此过程,直到它知道页面上每个组件的底层 DOM 标签元素。
这种递归遍历树以了解 React 应用组件树的底层 DOM 标签元素的确切过程称为协调 (reconciliation)。
协调结束时,React 知道了 DOM 树的结果,并且像 react-dom
或 react-native
这样的渲染器会应用更新 DOM 节点所需的最少更改。这意味着,当调用 ReactDOM.render()
或 setState()
时,React 会执行协调。
对于 setState
,它会遍历并通过比较新树和渲染树的差异来确定树中发生了哪些更改。然后,它将这些更改应用于当前树,从而更新与 setState()
调用对应的状态。
现在我们了解了协调是什么,让我们来看看这个模型的陷阱。
什么是 React 堆栈协调器?
哦,对了,为什么这个叫做"堆栈"协调器?这个名字来源于"堆栈"数据结构,它是一种后进先出的机制。
那么,堆栈和我们刚才看到的有什么关系呢?嗯,事实证明,由于我们实际上是在执行递归,所以它与堆栈息息相关。
React 中的递归是什么?
为了理解为什么会出现这种情况,让我们举一个简单的例子,看看调用堆栈中发生了什么:
jsx
function fib(n) {
if (n < 2){
return n
}
return fib(n - 1) + fib (n - 2)
}
fib(10)
如我们所见,调用栈会将每个 fib()
调用压入栈中,直到弹出 fib(1)
,这是第一个返回的函数调用。
然后,它继续压入递归调用,并在到达 return 语句时再次弹出。这样,它有效地利用了调用栈,直到 fib(3)
返回并成为最后一个从栈中弹出的项。

我们刚刚看到的协调算法是一种纯递归算法。更新会导致整个子树立即重新渲染。虽然这种方法效果很好,但也存在一些局限性。
正如 Andrew Clark 指出的那样,在 UI 中,没有必要每次更新都立即应用;事实上,这样做可能会造成资源浪费,导致掉帧并降低用户体验。
此外,不同类型的更新具有不同的优先级------动画更新必须比数据存储的更新更快完成。
丢帧问题
那么,我们所说的丢帧究竟是什么意思?为什么说这是递归方法的问题呢?为了理解这一点,我们先简单回顾一下帧率是什么,以及它从用户体验的角度为何如此重要。
帧率是什么?
帧速率是指连续图像在显示器上出现的频率。我们在电脑屏幕上看到的所有内容都是由图像或帧组成的,这些图像或帧以人眼瞬时呈现的速率在屏幕上播放。
为了理解这一点,可以把电脑显示器想象成一本翻页书,把翻页书的页面想象成在翻页时以一定速率播放的帧。
相比之下,电脑显示器只不过是一本自动翻页书,当屏幕上的内容发生变化时,它会连续播放。
通常,为了让人眼感觉视频流畅且瞬时,视频必须以大约 30 帧/秒 (FPS) 的速率播放;更高的帧速率会带来更好的体验。
如今,大多数设备的屏幕刷新率为 60 FPS,1/60 = 16.67 毫秒,这意味着每 16 毫秒显示一个新帧。这个数字很重要,因为如果 React 渲染器在屏幕上渲染某些内容的时间超过 16 毫秒,浏览器就会丢弃该帧。
但实际上,浏览器需要进行日常维护,因此所有工作都必须在 10 毫秒内完成。如果未能满足此预算,帧率就会下降,屏幕上的内容就会抖动。这通常被称为卡顿,会对用户体验产生负面影响。
当然,对于静态内容和文本内容来说,这并不是什么大问题。但在显示动画的情况下,这个数字至关重要。
如果 React 协调算法每次更新时都遍历整个 App
树并重新渲染,而遍历时间超过 16 毫秒,就会丢帧。
这就是为什么许多人希望更新按优先级分类,而不是盲目地应用传递给协调器的每个更新的一个重要原因。此外,许多人希望能够暂停并在下一帧恢复工作。这样,React 就可以更好地控制 16 毫秒的渲染预算。
这促使 React 团队重写了协调算法,并将其称为 Fiber。那么,让我们看看 Fiber 如何解决这个问题。
React Fiber 是如何工作的?
现在我们了解了 Fiber 的开发动机,让我们总结一下实现它所需的功能。再次强调,我引用了 Andrew Clark 的笔记:
- 为不同类型的工作分配优先级
- 暂停工作并稍后再回来
- 如果不再需要,则中止工作
- 重用之前完成的工作
实现此类功能的挑战之一是 JavaScript 引擎的工作原理以及该语言缺乏线程。为了理解这一点,让我们简要探讨一下 JavaScript 引擎如何处理执行上下文。
JavaScript 执行栈
每当你用 JavaScript 编写函数时,JavaScript 引擎都会创建一个函数执行上下文。
每次 JavaScript 引擎启动时,它都会创建一个全局执行上下文来保存全局对象;例如,浏览器中的 window
对象和 Node.js 中的 global
对象。JavaScript 使用一个堆栈数据结构(也称为执行栈)来处理这两个上下文。
因此,当你编写类似这样的代码时,JavaScript 引擎首先会创建一个全局执行上下文并将其推送到执行栈中:
jsx
function a() {
console.log("i am a")
b()
}
function b() {
console.log("i am b")
}
a()
然后,它为 a()
函数创建一个函数执行上下文。由于 b()
在 a()
内部被调用,因此它为 b()
创建另一个函数执行上下文并将其压入堆栈。
当 b()
函数返回时,引擎会销毁 b()
的上下文。当我们退出 a()
函数时,a()
上下文也会被销毁。执行期间的堆栈如下所示:

但是,当浏览器发出 HTTP 请求之类的异步事件时会发生什么?JavaScript 引擎会存储执行堆栈并处理异步事件,还是会等到事件完成?
JavaScript 引擎的做法有所不同:在执行堆栈的顶部,JavaScript 引擎有一个队列数据结构,也称为事件队列。事件队列处理异步调用,例如 HTTP 或进入浏览器的网络事件。

JavaScript 引擎通过等待执行堆栈清空来处理队列中的项目。因此,每次执行栈清空时,JavaScript 引擎都会检查事件队列,从队列中弹出项目并处理事件。
需要注意的是,JavaScript 引擎仅在执行栈为空或执行堆、栈中唯一的项目是全局执行上下文时才会检查事件队列。
虽然我们称之为异步事件,但这里有一个细微的区别:这些事件就其到达队列的时间而言是异步的,但就其实际处理时间而言并非真正异步的。
回到我们的堆栈协调器,当 React 遍历树时,它会在执行栈中进行。因此,当更新到达时,它们会到达事件队列(某种程度上)。并且,只有当执行堆栈清空时,更新才会被处理。
这正是 Fiber 通过几乎重新实现堆栈并添加智能功能(例如暂停、恢复和中止)来解决的问题。
再次引用 Andrew Clark 的话:"Fiber 是对堆栈的重新实现,专门用于 React 组件。你可以将单个 Fiber 视为一个虚拟堆栈框架。
"重新实现堆栈的优势在于,你可以将堆栈框架保存在内存中,并随时随地以任何方式执行它们。这对于实现我们的调度目标至关重要。"
"除了调度之外,手动处理堆栈框架还可以释放并发和错误边界等功能的潜力。我们将在后续章节中讨论这些主题。"
简单来说,Fiber 代表一个拥有自己虚拟堆栈的工作单元。在之前的协调算法实现中,React 创建了一个不可变的对象树(React 元素),并以递归方式遍历该树。
在当前的实现中,React 创建了一个可以变化的 Fiber 节点树。Fiber 节点有效地保存了组件的状态、props 以及它渲染到的底层 DOM 元素。
而且,由于 Fiber 节点可以变异,React 无需为更新重新创建每个节点;它只需在有更新时克隆并更新节点即可。
对于 Fiber 树,React 不会执行递归遍历。相反,它会创建一个单链表,并执行父节点优先、深度优先的遍历。
Fiber 节点的单链表
一个 Fiber 节点代表一个堆栈框架和一个 React 组件的实例。一个 Fiber 节点包含以下成员:
- Type
- Key
- Child
- Sibling
- Return
- Alternate
- Output
译者注:
Type
→ Fiber 节点所对应的 React 元素类型,用于区分 DOM、函数组件、类组件
key
→ React 的 key,用于区分同级节点
child
→ 第一个子 Fiber 节点
sibling
→ 下一个兄弟节点
return
→ 父节点
alternate
→ 上一次渲染的 Fiber(用于 diff)
output
→ 渲染结果(通常是 DOM 元素或者 null)
Type
例如 <div>
和 <span>
,宿主组件(字符串)、类、函数组件或复合组件。
译者注:宿主组件,也就是直接对应原生 DOM 元素的组件
Key
该键与我们传递给 React 元素的键相同。
Child
表示我们在组件上调用 render()
时返回的元素:
jsx
const Name = (props) => {
return(
<div className="name">
{props.name}
</div>
)
}
<Name>
的 Child 是 <div>
,因为它返回一个 <div>
元素。
Sibling
表示 render
返回元素列表的情况:
jsx
const Name = (props) => {
return([<Customdiv1 />, <Customdiv2 />])
}
在上面的例子中,<Customdiv1>
和 <Customdiv2>
是 <Name>
的子元素,而 <Name>
是父元素。这两个子元素构成一个单链表。
Return
Return 是 Return 到堆栈框架,它是逻辑上返回到父级 Fiber 节点,因此代表父级。
pendingProps
和 memoizedProps
记忆化 (Memoization) 是指存储函数执行结果的值,以便稍后使用,从而避免重复计算。pendingProps
表示传递给组件的 props,而 memoizedProps
则在执行堆栈的末尾初始化,存储此节点的 props。
当传入的 pendingProps
等于 memoizedProps
时,表示 Fiber 的先前输出可以重用,从而避免不必要的工作。
pendingWorkPriority
pendingWorkPriority
是一个数字,表示 Fiber 所代表的工作的优先级。ReactPriorityLevel 模块列出了不同的优先级及其含义。除了 NoWork 为零之外,数字越大表示优先级越低。
例如,您可以使用以下函数来检查 Fiber 的优先级是否至少与给定的优先级相同。调度程序使用优先级字段来搜索下一个要执行的工作单元:
jsx
function matchesPriority(fiber, priority) {
return fiber.pendingWorkPriority !== 0 &&
fiber.pendingWorkPriority <= priority
}
Alternate
任何时刻,一个组件实例最多有两棵与其对应的 Fiber 树:current fiber 和 in-progress fiber。current fiber 和 in-progress fiber 互相替代。
current fiber 表示已渲染的内容,而 in-progress fiber 在概念上是尚未返回的堆栈帧。
Output
Output 是 React 应用程序的叶节点。它们特定于渲染环境(例如,在浏览器应用中,它们是 div 和 span)。在 JSX 中,它们使用小写的标签名称表示。
从概念上讲,Fiber 的输出是函数的返回值。每个 Fiber 最终都有一个输出,但 Output 仅由宿主组件在叶节点处创建。然后,Output 沿树向上传递。
Output 最终传递给渲染器,以便它可以将更改刷新到渲染环境中。例如,让我们看一下 Fiber 树如何查找包含以下代码的应用:
jsx
const Parent1 = (props) => {
return([<Child11 />, <Child12 />])
}
const Parent2 = (props) => {
return(<Child21 />)
}
class App extends Component {
constructor(props) {
super(props)
}
render() {
<div>
<Parent1 />
<Parent2 />
</div>
}
}
ReactDOM.render(<App />, document.getElementById('root'))
我们可以看到,Fiber 树由相互链接的子节点(兄弟关系)的单链表和父子关系的链表组成。可以使用深度优先搜索来遍历这棵树。

渲染阶段
为了理解 React 如何构建这棵树并对其执行协调算法,让我们在 React 源代码中查看一个单元测试,并附加一个调试器来跟踪整个过程;您可以克隆 React 源代码并导航到此目录。
首先,添加一个 Jest 测试并附加一个调试器。这是一个用于渲染带有文本的按钮的简单测试。当您点击按钮时,应用会销毁按钮并渲染一个带有不同文本的 <div>
,因此这里的文本是一个状态变量:
jsx
'use strict';
let React;
let ReactDOM;
describe('ReactUnderstanding', () => {
beforeEach(() => {
React = require('react');
ReactDOM = require('react-dom');
});
it('works', () => {
let instance;
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
text: "hello"
}
}
handleClick = () => {
this.props.logger('before-setState', this.state.text);
this.setState({ text: "hi" })
this.props.logger('after-setState', this.state.text);
}
render() {
instance = this;
this.props.logger('render', this.state.text);
if(this.state.text === "hello") {
return (
<div>
<div>
<button onClick={this.handleClick.bind(this)}>
{this.state.text}
</button>
</div>
</div>
)} else {
return (
<div>
hello
</div>
)
}
}
}
const container = document.createElement('div');
const logger = jest.fn();
ReactDOM.render(<App logger={logger}/>, container);
console.log("clicking");
instance.handleClick();
console.log("clicked");
expect(container.innerHTML).toBe(
'<div>hello</div>'
)
expect(logger.mock.calls).toEqual(
[["render", "hello"],
["before-setState", "hello"],
["render", "hi"],
["after-setState", "hi"]]
);
})
});
在初始渲染中,React 会创建一个初始渲染的当前树。
createFiberFromTypesAndProps()
函数使用来自特定 React 元素的数据创建每个 React Fiber。运行测试时,在此函数处设置一个断点,并查看调用堆栈:

我们可以看到,调用堆栈回溯到 render()
调用,最终到达 createFiberFromTypeAndProps()
。这里还有一些其他值得关注的函数:workLoopSync()
、performUnitOfWork()
和 beginWork()
。
workLoopSync()
workLoopSync()
是 React 开始构建树的时候,它从 <App>
节点开始,递归地移动到 <div>
、<div>
和 <button>
,它们是 <App>
的子节点。workInProgress 函数持有对下一个有工作要做的 Fiber 节点的引用。
performUnitOfWork()
performUnitOfWork()
接受一个 Fiber 节点作为输入参数,获取该节点的替代节点,然后调用 beginWork()
。这相当于在执行堆栈中启动函数执行上下文的执行。
beginWork()
当 React 构建树时,beginWork()
会直接调用 createFiberFromTypeAndProps()
并创建 Fiber 节点。React 递归地执行工作,最终 performUnitOfWork()
返回 null,表示它已到达树的末尾。
使用 instance.handleClick()
现在,当我们执行 instance.handleClick()
时,点击按钮并触发状态更新会发生什么?在这种情况下,React 会遍历 Fiber 树,克隆每个节点,并检查是否需要在每个节点上执行任何工作。
我们查看此场景的调用堆栈,它看起来像这样:

虽然我们在第一个调用堆栈中没有看到 completeUnitOfWork()
和 completeWork()
,但我们可以在这里看到它们。与 performUnitOfWork()
和 beginWork()
一样,这两个函数执行当前执行的完成部分,这意味着返回堆栈。
正如我们所见,这四个函数共同执行工作单元,并控制当前正在执行的工作,这正是堆栈协调器所缺少的。
下图显示每个 Fiber 节点由完成该工作单元所需的四个阶段组成。

这里需要注意的是,每个节点只有在其子节点和兄弟节点返回 completeWork()
后才会进入 completeUnitOfWork()
阶段。
例如,它首先对 <App/>
执行 performUnitOfWork()
和 beginWork()
,然后对 Parent1 执行 performUnitOfWork()
和 beginWork()
,依此类推。一旦 <App/>
的所有子节点都完成了工作,它就会返回并完成 <App>
上的工作。
此时,React 完成了其渲染阶段。基于 click()
更新新建的树称为 workInProgress
树。这基本上是等待渲染的草稿树。
提交阶段
一旦渲染阶段完成,React 将进入提交阶段,在此阶段它基本上交换当前树和 workInProgress 树的根指针,从而有效地将当前树与基于 click() 更新构建的草稿树交换。

不仅如此,React 还会在将指针从根节点交换到 workInProgress 树后,重用旧的当前状态。此优化过程的最终效果是应用程序从上一个状态到下一个状态再到下一个状态的平滑过渡,依此类推。
那么 16 毫秒的帧时间呢?React 会为每个正在执行的工作单元有效地运行一个内部计时器,并在执行工作时持续监控此时间限制。
时间耗尽后,React 会暂停当前工作单元,将控制权交还给主线程,并让浏览器渲染此时完成的内容。
然后,在下一帧中,React 会从上次中断的地方继续构建树。当有足够的时间时,它会提交 workInProgress 树并完成渲染。
React v16 以来的变化和改进
自 React v16 推出以来,React Fiber 经历了多项重大改进和变化,其中影响最为深远的更新出现在 React v18 中。这些改进主要旨在提升性能和开发者体验,同时解决异步渲染和并发相关的问题。
以下是自 React v16 以来的主要改进和变化:
并发渲染
React Fiber 自诞生以来最重要的更新就是并发性。这是对 React 核心渲染模型的一次基础性更新,它利用 React Fiber 的功能提供可中断渲染。
并发模式允许 React 根据优先级暂停、中止或恢复渲染更新,并同时处理多个更新,而无需等待一个更新完成后再启动另一个更新。这与传统的同步渲染模型形成了鲜明对比,在传统的同步渲染模型中,更新在单个不间断的同步事务中渲染。
虽然并发模式最初是在 React v16 中与 React Fiber 架构一起开发的,但它在 React v18 中正式发布,成为核心渲染机制,并提供了以下关键功能:
-
时间分片------这使得 React 可以将渲染任务分解成多个块,并将它们分散到多个帧中,从而防止应用在大规模更新期间无响应。
-
选择性渲染------在客户端渲染服务器端内容时,React 现在可以仅渲染 UI 中立即需要的部分。
-
延迟渲染------如果 React 确定某个更新对于用户体验来说并非立即需要,它可以延迟该更新的渲染。这使得 React 可以专注于更重要的更新,并防止 UI 无响应。
自动批处理
自动批处理是 React Fiber 底层改进的直接成果,它有助于减少状态更改时重新渲染的次数,因为它允许 React 将多个状态更新组合到一次重新渲染中。
例如,当您在单个渲染周期中调用多个 setState 函数时,React 会自动将它们合并到一次更新中。以下代码示例展示了这一点:
jsx
function MyComponent() {
const [count, setCount] = useState(0);
const handleClick = () => {
// Multiple state updates within a single event handler
setCount(count + 1);
setCount(count + 2);
};
console.log("Rendering")
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
handleClick 函数调用 setCount 两次,一次将计数加 1,另一次加 2。
React 会自动将这两次状态更新合并为一次更新。这意味着只会发生一次重新渲染,并且"渲染"操作只会在终端中记录一次。
在 React v18 发布之前,批量更新是在 React 事件处理程序内部执行的。这意味着在 React 生命周期之外(例如在 setTimeout 或 Promise 内部),更新不会自动批量执行。React Fiber 在 v18 中改进了更新,现在所有上下文(包括异步代码、setTimeout、Promise 和原生事件处理程序)都会批量执行更新。
Suspense
Suspense 是一项与并发模式紧密相关的功能,最初在 React v16 中随 Fiber 引入,但其功能在 React v17 和 v18 的更新中得到了极大的扩展。
自首次发布以来,该功能经历了显著的改进。它最初设计用于使用 React.lazy 管理组件的延迟加载(代码拆分),但其功能得到了扩展,尤其是在 v18 中引入并发模式之后。
现在,Suspense 在处理数据获取等异步操作中发挥着关键作用,并在这些操作期间提供对 UI 渲染的更精细控制。您可以声明式地指定当树的某个部分尚未准备好渲染时 React 应该显示的内容。
这可以通过使用 fallback 选项来实现,该选项会在异步任务运行时渲染组件或字符串:
jsx
<Suspense fallback={<div>Loading data...</div>}>
<SomeComponent />
</Suspense>
另一项关键改进是服务器端渲染 (SSR) 中的流式传输和选择性内容补充。借助这项改进,服务器可以向客户端发送初始的轻量级 HTML shell,并在数据可用时分块补充内容。
React 可以选择性地补充应用中用户立即可见的部分,同时暂停其余部分,直到必要的数据或资源准备就绪。
Transitions
Transitions 是基于 React Fiber 构建的全新 API,它使用自身的调度算法来区分更新并将其标记为紧急或非紧急。
紧急更新包括用户输入或动画等需要立即反馈的任务。非紧急更新(例如渲染搜索结果列表)的优先级较低,可以接受轻微的延迟。
Transitions API 包含一个 useTransition hook,它允许您将状态更新标记为非紧急,因为默认情况下,所有未标记的状态更新都被视为紧急。
useTransition
useTransition hook 以非阻塞方式处理 UI 状态之间的过渡。它返回一个包含两个值的数组:
- isPending - 一个布尔值,指示过渡是否正在进行
- startTransition - 一个用于启动过渡的函数
为了了解该 hook 的工作原理,想象一下一个组件,它显示一个可过滤的长列表。如果没有 useTransition,更新过滤器可能会导致体验卡顿,因为每次过滤器更改时,React 都会重新渲染这个长列表。
使用 useTransition,我们可以将呈现过滤列表的更新标记为非紧急,从而保持用户输入的响应:
jsx
import React, { useState, useTransition } from 'react';
const FilteredList = ({ items }) => {
const [filter, setFilter] = useState('');
const [filteredItems, setFilteredItems] = useState(items);
const [isPending, startTransition] = useTransition();
const handleFilterChange = (e) => {
const value = e.target.value;
setFilter(value);
// Mark this update as non-urgent
startTransition(() => {
const updatedItems = items.filter(item =>
item.toLowerCase().includes(value.toLowerCase())
);
setFilteredItems(updatedItems);
});
};
return (
<div>
<input
type="text"
value={filter}
onChange={handleFilterChange}
placeholder="Filter items"
/>
{isPending && <p>Updating list...</p>}
<ul>
{filteredItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
};
export default FilteredList;
在此示例中,setFilter 会立即更新,因为用户迫切希望看到他们的输入反映在输入框中;而 setFilteredItems 则被包裹在 startTransition 中,允许 React 在系统准备就绪时更新已过滤的列表。
isPending 用于在过滤过程需要时间时显示加载消息。这将有助于在列表在后台更新时向用户提供反馈,从而提升用户体验。