讲解
React 18 引入并发渲染,19 里继续完善。要解决的问题:一次状态更新触发的重渲染太贵(比如输入搜索词同时过滤十万行列表),输入框都会被卡得打不出字。并发渲染让更新分优先级——紧急更新(输入框回显)立即执行,非紧急更新(大列表重算)可以被打断、延后、甚至放弃,浏览器主线程始终优先响应用户输入。
两个面向开发者的 API。useTransition:const [isPending, startTransition] = useTransition(),把非紧急的 setState 包进 startTransition(() => setFiltered(...)),isPending 在过渡期间为真(可以显示「搜索中」的轻提示)。关键是它让更新可中断:用户继续输入时,上一次还没算完的过渡会被丢弃,直接开始新的——不会排队卡顿。
useDeferredValue 是另一个方向的等效工具:const deferredQuery = useDeferredValue(query),返回一个「滞后版本」的值——输入立刻更新 query,deferredQuery 等浏览器有空闲再跟上,用它做昂贵渲染的输入。列表组件 memo 之后配合 deferredValue,就实现了「输入流畅 + 列表稍后刷新」。
什么时候用?标准场景:输入驱动的昂贵渲染(搜索过滤、大表格、图表重算)、tab 切换大面板。什么时候不用?普通表单、小列表——过渡本身有调度开销,便宜更新加了反而慢。判断依据永远是:不卡就不用加,卡了先 memo/优化算法,还不够再上过渡。
示例
用任务队列模拟「可中断的低优先级更新」:紧急更新插队,过期过渡被丢弃(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// 迷你调度器:紧急任务立即跑,过渡任务可被新任务取代
function createScheduler() {
const log = [];
let pendingTransition = null;
return {
urgent(label) {
log.push('紧急: ' + label);
},
transition(label, work) {
if (pendingTransition) log.push('丢弃过期的过渡: ' + pendingTransition);
pendingTransition = label;
work();
log.push('过渡完成: ' + label);
pendingTransition = null;
},
log,
};
}
const sched = createScheduler();
let input = '';
let list = [];
// 用户连续输入 "r" → "re" → "rea"
for (const ch of ['r', 're', 'rea']) {
input = ch;
sched.urgent('输入框回显 "' + ch + '"'); // 紧急:立即
sched.transition('过滤 "' + ch + '"', () => {
list = ['react', 'redux', 'reselect'].filter((x) => x.startsWith(ch));
});
}
assert.deepStrictEqual(
sched.log.filter((l) => l.startsWith('紧急')),
['紧急: 输入框回显 "r"', '紧急: 输入框回显 "re"', '紧急: 输入框回显 "rea"'],
);
assert.deepStrictEqual(list, ['react']); // 最终结果按最新输入 'rea' 过滤(reselect 以 res 开头被排除)
// isPending 语义:过渡期间为 true
function trackPending() {
const states = [];
let isPending = false;
states.push(isPending); // 开始前
isPending = true;
states.push(isPending); // 过渡中
isPending = false;
states.push(isPending); // 完成后
return states;
}
assert.deepStrictEqual(trackPending(), [false, true, false]);
console.log('调度日志:', sched.log.join(' | '));
console.log('输入框始终即时响应,列表按最新输入 "rea" 过滤:', list.join('、'));
真实代码:
import { memo, useDeferredValue, useState, useTransition } from 'react';
const BigList = memo(function BigList({ query, items }) {
const filtered = items.filter((i) => i.name.includes(query)); // 十万行过滤
return filtered.slice(0, 50).map((i) => <li key={i.id}>{i.name}</li>);
});
// 方案一:useTransition
function SearchA({ items }) {
const [query, setQuery] = useState('');
const [listQuery, setListQuery] = useState('');
const [isPending, startTransition] = useTransition();
return (
<>
<input
value={query}
onChange={(e) => {
setQuery(e.target.value); // 紧急:输入框立即更新
startTransition(() => setListQuery(e.target.value)); // 非紧急:可被打断
}}
/>
{isPending && <span>搜索中…</span>}
<BigList query={listQuery} items={items} />
</>
);
}
// 方案二:useDeferredValue(组件管不住 setState 时用)
function SearchB({ items }) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query); // 滞后版输入
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<BigList query={deferredQuery} items={items} />
</>
);
}
常见坑
- 给便宜更新加 transition:调度开销超过渲染本身,小列表反而变卡;只给「明显卡」的更新用。
- memo 没配合:useDeferredValue 后列表组件不 memo,每次父渲染照样全量重渲染,defer 白搭。
- 把输入框 state 也放进 transition:输入回显必须紧急,否则打字都卡——transition 只包非紧急的派生更新。
- 以为 isPending 等于 loading:它是「过渡进行中」,不是请求中;数据请求的加载态另算。
- 期待 transition 加速计算:它不减少计算量,只是不让计算阻塞交互;计算本身太贵还是要优化算法/虚拟列表。
小结
useTransition 把非紧急更新标记为可打断,useDeferredValue 给值一个滞后版本;输入回显永远紧急;memo 配合才有效。最后一章:构建与部署。