讲解

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 配合才有效。最后一章:构建与部署。