引言
在 React18
中最引人瞩目的要数 Concurrent Mode
(并发模式), 它并不是一个具体功能, 而是一个底层设计!! 在 Concurrent Mode
下更新的 reconcile
(调度)过程被认为是可中断的, 这就使得浏览器渲染进程不会被一个耗时较长的 reconcile
(调度) 阻塞而导致页面在交互过程中出现卡顿, 同时将任务按照优先级进行了划分, 让更高优先级的更新优先被处理!!
对于 React 18
可通过新的 Render API
来启用 Concurrent Mode
(并发模式)
js
import React from 'react';
import ReactDOM from 'react-dom/client';
const root = document.getElementById('root')!;
ReactDOM.createRoot(root).render(<App />);
但是呢? 如果真正要触发 并发更新
还需要配合 useTransition
或者 useDeferredValue
进行使用, 而这两个 API
的使用也正是我们今天要讲的主角!!!
一、从一个 DEMO 开始
在开始前, 我们先看一个例子, 如下代码所示:
- 输入框
Input
输入词条将调用handleChange
修改状态keyword
的值 Card
中渲染了300
条数据, 每条数据在渲染时手动延迟1ms
, 展示内容为输入词条: {keyword}
, 当然这里进行了组件的拆分, 同时使用了memo
来减少一些不必要的渲染
js
const Item = ({ keyword }) => {
// 增加 1ms 渲染时间s
let startTime = performance.now();
while (performance.now() - startTime < 1) {}
return <div>输入词条: {keyword}</div>;
};
const List = memo(({ keyword }) => {
return Array(300)
.fill("a")
.map((v, index) => <Item key={index} keyword={keyword} />);
});
export default function App() {
const [keyword, setKeyword] = useState(null);
const handleChange = useCallback((e) => {
setKeyword(e.target.value);
}, []);
return (
<div className="App">
<Card
bordered={false}
title={<Input placeholder="输入词条" onChange={handleChange} />}
>
<List keyword={keyword} />
</Card>
</div>
);
}
上面 👆🏻 代码运行结果可想而知, 我们如果在输入框中输入数据, 会很明显感觉到卡顿

而这里造成卡顿的原因其实也简单: 由于浏览器是单线程的, 在我们输入词后 React
列表会进行重新渲染(花费较长时间), 导致输入框无法及时响应我们的交互, 从而给用户造成了页面卡顿的现象
二、useTransition 改造
2.1 简介
useTransition
是一个React Hook
, 可让我们实现在不阻塞UI
的情况下更新状态
js
import { useTransition } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
// ...
}
- 如上代码, 调用该
Hook
:
- 不接受任何参数
- 返回一个包含两个元素的数组, 第一个字段(
isPending
)表示是否等待中, 第二参数(startTransition
) 接收一个函数参数, 函数中修改状态的任务将会被标记为低优先级(不紧急)任务, 同时在修改状态执行任务其中随时会被中断停止, 执行高优先级的任务, 从而避免阻塞用户交互、UI
渲染等高优先级任务
js
import { useTransition, useCallback, useState } from 'react';
function TabContainer() {
const [num, setNum] = useState(0)
const [isPending, startTransition] = useTransition();
const handleChange = useCallback((v) => {
startTransition(() => {
// 该任务被标记为低优先级任务, 不阻塞用户交互、`UI` 渲染、高优先级任务
setNum(v)
})
}, [])
// ...
}
- 注意事项:
useTransition
是一个Hook
, 所以只能在函数组件内部或者自定义Hooks
中调用, 如果需要在其他地方(例如类组件)可以使用独立模式,React
提供了一个专门的api
startTransition
来实现- 传递给
startTransition
的函数必须是同步的 startTransition
中被标记的状态更新
任务, 会被其他高优先级
任务中断
2.2 改造
如下代码, 我们使用 useTransition
进行了简单的改造, handleChange
中使用 startTransition
并在其回调函数中调用 setKeyword
修改状态, 如此修改状态也就是 setKeyword
的任务将会被标记为非紧急, 会优先响应用户的交互, 从而及时响应用户的操作
diff
export default function App() {
+ const [isPending, startTransition] = useTransition();
const [keyword, setKeyword] = useState(null);
+ const handleChange = useCallback(
+ (e) => {
+ startTransition(() => setKeyword(e.target.value));
+ },
+ [startTransition]
+ );
return (
<div className="App">
<Card
bordered={false}
title={<Input placeholder="输入词条" onChange={handleChange} />}
>
<List keyword={keyword} />
</Card>
</div>
);
}
最后的效果如下, 在我们快速输入词条的情况下, 输入框会及时响应用户, 下面的展示区域则会延迟进行展示

下面我们可以借用 isPending
为展示区域设置一个 pending
效果, 修改代码如下:
diff
export default function App() {
const [isPending, startTransition] = useTransition();
const [keyword, setKeyword] = useState(null);
const handleChange = useCallback(
(e) => {
startTransition(() => setKeyword(e.target.value));
},
[startTransition]
);
return (
<div className="App">
<Card
bordered={false}
title={<Input placeholder="输入词条" onChange={handleChange} />}
>
+ <Spin spinning={isPending}>
+ <List keyword={keyword} />
+ </Spin>
</Card>
</div>
);
}
最后效果如下, 当我们快速输入词条, 输入框能够正常反馈, 展示区域将延迟展示, 同时还会显示加载状态

三、 useDeferredValue 改造
3.1 简介
useDeferredValue
是一个React Hook
, 可以让某个值(可以是props
也可以是state
)延迟更新, 从而推迟部分UI
的更新
js
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}
- 如上代码, 该
hooks
使用起来其实也很简单:
- 接收一个参数, 也就是需要延迟生效的状态或者
props
- 返回一个值, 可以理解为是新的状态, 该状态是会
延迟更新
的
3.2 改造
如下代码, 使用 useDeferredValue
基于 keyword
生成新的值 deferredKeyword
, 只是 deferredKeyword
值的更新任务会被标记为低优先级, 从而避免阻塞用户的交互
diff
export default function App() {
const [keyword, setKeyword] = useState(null);
+ // 根据 keyword 延迟更新
+ const deferredKeyword = useDeferredValue(keyword);
const handleChange = useCallback((e) => {
setKeyword(e.target.value);
}, []);
return (
<div className="App">
<Card
bordered={false}
title={<Input placeholder="输入词条" onChange={handleChange} />}
>
+ <List keyword={deferredKeyword} />
</Card>
</div>
);
}
最终效果如下, 输入框快速输入, 页面能够正常响应, 展示列表延迟在空闲时才会进行更新(延迟更新)

这里我们可以尝试在 useEffect
中打印下 keyword
和 deferredKeyword
diff
export default function App() {
const [keyword, setKeyword] = useState(null);
// 根据 keyword 延迟更新
const deferredKeyword = useDeferredValue(keyword);
const handleChange = useCallback((e) => {
setKeyword(e.target.value);
}, []);
+ useEffect(() => {
+ console.log("keyword: ", keyword);
+ console.log("deferredKeyword: ", deferredKeyword);
+ }, [keyword, deferredKeyword]);
return (
<div className="App">
<Card
bordered={false}
title={<Input placeholder="输入词条" onChange={handleChange} />}
>
<List keyword={deferredKeyword} />
</Card>
</div>
);
}
从打印结果来看, 在快速输入情况下 deferredKeyword
一直为空, 只有在停止输入(空闲)时才会进行更新

3.3 useDeferredValue 实现
设想下, 如果让你实现 useDeferredValue
你会怎么做呢? 从它的特性也就是「依赖某个值」「延迟更新」「新的状态」 这几点出发, 其实我们也能猜到大概的一个实现代码:
- 新建一个状态值
newValue
- 使用
useTransition
来延迟更新状态值newValue
- 使用
useEffect
监听依赖值value
的变更, 并调用startTransition
来延迟更新新状态
js
const useDeferredValue = (value) => {
const [newValue, setNewValue] = useState(value)
const [isPending, startTransition] = useTransition();
useEffect(() => {
startTransition(() => {
setNewValue(value)
})
}, [value, startTransition])
return newValue
}
四、和防抖、节流的区别
-
防抖: 等待一段时间(例如一秒钟), 用户没有任何操作(触发事件), 则执行事件处理函数
-
节流: 指定时间段内(例如一秒钟), 用户多次操作(触发事件), 只会执行第一次事件处理函数
-
而本文提到的不管是
useDeferredValue
还是useTransition
都是借用并发更新特性, 将任务标记为低优先级, 将主线程让给其他更高优先级任务, 从而保障用户的一个交互体验!!
- 与防抖或节流不同, 它不需要选择任何固定延迟!!! 如果用户的设备速度很快(例如功能强大的笔记本电脑), 则延迟几乎会立即发生并且不会被注意到。如果用户的设备速度很慢, 则延迟较为明显。
- 此外, 与防抖或节流不同,
useDeferredValue
或者useTransition
默认情况下是可随时中断的, 这意味着如果React
正在重新渲染一个大列表, 但用户再次进行击键,React
将放弃本次低优先级任务, 优先响应用户操作, 然后再继续执行低优先级任务。同样情况再防抖和节流仍然会产生卡顿, 因为它们执行期间是无法被停止的。