Skip to content

Transition 与优先级 ​

问题:输入和慢列表为何不该同等紧急 ​

受控输入必须立即反映每次按键;几千项列表晚一拍通常可以接受。如果同一个更新同时驱动二者,昂贵渲染会拖慢输入。Transition 标记的是“可以被更紧急更新打断并重新开始”的 state 更新。

完整实验:输入立即更新,列表允许追赶 ​

jsx
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;它不替代算法优化、虚拟列表、缓存或性能测量。

练习:契约任务 ​

把示例改为“紧急备注输入 + 非紧急排序模式”:

  1. 备注输入始终立即回显。
  2. 两个按钮切换 name / name-desc,排序慢列表的更新放入 Transition。
  3. pending 时按钮禁用并显示“排序中”,提交后显示首项。
  4. 排序不能修改原数组;同一按钮重复点击结果稳定。

完成后对照答案 05-01。

下一篇 ​

Transition 只安排更新;Suspense 负责渲染依赖尚未就绪时的占位边界。

用心记录每一步成长