Transition 与优先级
问题:输入和慢列表为何不该同等紧急
受控输入必须立即反映每次按键;几千项列表晚一拍通常可以接受。如果同一个更新同时驱动二者,昂贵渲染会拖慢输入。Transition 标记的是“可以被更紧急更新打断并重新开始”的 state 更新。
完整实验:输入立即更新,列表允许追赶
import { memo, useState, useTransition } from 'react'
const cities = Array.from({ length: 300 }, (_, index) =>
`City ${String(index).padStart(4, '0')}`,
)
function SlowRow({ city, query }) {
const started = performance.now()
while (performance.now() - started < 1) {
// 每项约 1ms,仅用于本实验;实际应用应删除
}
return city.toLowerCase().includes(query) ? <li>{city}</li> : null
}
const SlowList = memo(function SlowList({ query }) {
const normalized = query.toLowerCase()
return <section>
<p>列表查询:{query || '空'}</p>
<ul>{cities.map(city => <SlowRow key={city} city={city} query={normalized} />)}</ul>
</section>
})
export default function App() {
const [input, setInput] = useState('')
const [listQuery, setListQuery] = useState('')
const [isPending, startTransition] = useTransition()
function handleChange(event) {
const next = event.target.value
setInput(next)
startTransition(() => {
setListQuery(next)
})
}
return (
<main>
<h1>城市筛选</h1>
<input aria-label="查询" value={input} onChange={handleChange} />
<p>{isPending ? '列表正在追赶…' : '列表已同步'}</p>
<SlowList query={listQuery} />
</main>
)
}输入 123 时,setInput 是紧急更新,setListQuery 属于 Transition。若第二次按键到来时第一轮慢列表仍在渲染,React 可放弃旧工作并从新查询重来;只有完成的树才提交 DOM。因此并发是调度和可中断渲染,不是两个线程同时改页面。
先等待初始列表出现,再连续输入。第一次紧急渲染使用新 input 和旧 listQuery;memo 发现 SlowList 的 query 未变,可以跳过整棵慢子树。随后过渡渲染使用新 listQuery,React 才处理各个 SlowRow。这里依赖的是第三章的记忆化边界,不能只给 setter 套一个 Transition 就期待所有子组件自动跳过。
中断发生在 React 工作之间,不在任意一行 JavaScript 中间
每个 <SlowRow /> 让 React 分别调用一个组件,形成可调度的工作单元。不要把它写成 SlowRow({ city, query }),也不要把全部慢计算塞进 SlowList 的一次 filter:那会成为一个长同步函数,React 必须等它返回才能接着调度。即使放在 Transition 中,单个 300ms 循环仍能阻塞 300ms。
做两个对照:暂时去掉 memo,紧急更新也会重新执行旧查询的慢树;再把 300 次小延迟改为 SlowList 内一次 300ms 等待,即使保留 memo,后台那次长函数执行时仍接不了新按键。恢复原写法后,用浏览器 Performance 查看短组件工作间的调度机会。调度不承诺每台设备的精确毫秒数,也没有消除首次加载的总成本;最终 DOM 提交仍是同步过程。
规则与过程
startTransition(action) 会立即执行 action,以便标记其中同步触发的更新;它不是延迟执行的计时器。旧内容可以继续显示,isPending 在过渡工作未提交时提示用户。
受控输入本身不能放入 Transition:如果把 setInput(next) 也标成非紧急,输入值会落后于按键。没有 isPending 需求且在组件外触发更新时,可使用顶层 startTransition;组件内通常用 useTransition。
关键变体
- Transition 更新可能被紧急更新打断,并会重新开始渲染。
- 多个同时进行的 Transition 当前可能被批在一起,不能把 pending 当作精确任务计数器。
- React 19 支持异步 Action;但
await之后的普通 state 更新在当前规则下需要再包一层startTransition,除非使用已为你管理 Action 的接口。 - Transition 不会让循环、JSON 解析或第三方脚本跑到后台线程。真正的 CPU 并行需要 Web Worker 等平台能力。
边界
不要为每次状态更新都加 Transition。即时反馈、焦点、键盘回显和拖拽位置应保持紧急。Transition 适合导航、切换大视图和昂贵派生 UI;它不替代算法优化、虚拟列表、缓存或性能测量。
练习:契约任务
把示例改为“紧急备注输入 + 非紧急排序模式”:
- 备注输入始终立即回显。
- 两个按钮切换
name/name-desc,排序慢列表的更新放入 Transition。 - pending 时按钮禁用并显示“排序中”,提交后显示首项。
- 排序不能修改原数组;同一按钮重复点击结果稳定。
完成后对照答案 05-01。
下一篇
Transition 只安排更新;Suspense 负责渲染依赖尚未就绪时的占位边界。