跳至正文
来两杯美式
返回

React 核心(14):React 18 并发特性

By 来两杯美式
发布于

并发渲染是 React 18 的核心架构升级——渲染可以被中断、恢复、放弃,让紧急更新优先响应,非紧急更新延后处理。

一、什么是并发渲染

1.1 从”一口气跑完”到”跑一段歇一段”

React 18 之前,一旦开始渲染,就必须一口气跑完——中间不能停、不能断、不能让路。

React 18 引入了并发渲染(Concurrent Rendering):渲染过程可以被中断、恢复、甚至放弃。

类比:并发渲染像交通灯调度——紧急车辆(用户输入)来了,普通车辆(列表过滤)让路;紧急车辆过了,普通车辆继续走。

React 17 渲染时间线:
|--- 渲染(不可中断,50ms)---------------------------------------|
    用户输入被阻塞 ❌    动画掉帧 ❌

React 18 并发渲染时间线:
|-- 渲染片段 1 --|  处理用户输入  |-- 渲染片段 2 --|  渲染帧  |-- 片段 3 --|
                   ✅ 立即响应        ✅ 不掉帧

1.2 并发不是多线程

并发渲染仍然是单线程的。React 没有引入 Web Worker 或多线程渲染,它做的只是更智能的任务调度——把渲染拆成小片段,遇到更紧急的任务就让路。

并发渲染的本质是协作式多任务——React 主动让出主线程,靠的是 Fiber 架构提供的可中断渲染能力。

1.3 并发渲染的三个能力

  1. 中断:渲染到一半可以停下来,让主线程处理更紧急的事
  2. 恢复:停下来之后,下次从断点继续,不用从头来
  3. 放弃:如果中途有了新的更新,旧的渲染可以直接丢弃

这三个能力不是新 API,而是底层行为的变化。你只需要用新 API(useTransition、useDeferredValue)告诉 React 哪些更新优先级低

二、与 Fiber 的关系

2.1 Fiber 是基础设施,并发特性是上层 API

React渲染机制深入-Fiber与调度 中详细讲了 Fiber——React 16 引入的可中断渲染引擎。但 React 16/17 有了 Fiber,却没有真正”并发”——Fiber 只是具备了中断能力,默认行为仍然是同步渲染。

React 18 做的事情:把 Fiber 的中断能力真正用起来,并给开发者提供上层 API 控制优先级。

┌─────────────────────────────────────┐
│  用户层 API                          │
│  useTransition / useDeferredValue    │  ← React 18 新增
├─────────────────────────────────────┤
│  并发渲染调度器                       │
│  优先级调度 / 时间切片 / 可中断渲染    │  ← React 18 新增
├─────────────────────────────────────┤
│  Fiber 架构                          │
│  链表遍历 / 双缓冲 / 可中断单元       │  ← React 16 已有
└─────────────────────────────────────┘

类比:Fiber 是修好的高速公路(路能走),React 18 是装了智能红绿灯(知道谁先走)

2.2 为什么 React 16/17 没有并发

Fiber 是必要条件,但不是充分条件。并发渲染还需要:

这些 React 18 才完善。

三、Automatic Batching

3.1 什么是批量更新

多次 setState 调用,只触发一次重新渲染

function handleClick() {
  setCount(1); // 不会立即渲染
  setFlag(true); // 不会立即渲染
  setName("yi"); // 不会立即渲染
  // 三次 setState 合并,只触发一次渲染 ✅
}

3.2 React 17 的局限:只在 React 事件中批量

React 17 的批量更新只在 React 事件处理器中生效。在 setTimeout、Promise、原生事件中,每次 setState 都触发一次渲染:

// ❌ React 17:setTimeout 中不批量,触发 3 次渲染
setTimeout(() => {
  setCount(1); // 渲染 1
  setFlag(true); // 渲染 2
  setName("yi"); // 渲染 3
}, 1000);

原因:React 17 的批量更新依赖合成事件系统的包装,异步回调不在 React 控制范围内。

3.3 React 18:所有场景都自动批量

React 18 用并发渲染的调度机制替代了依赖合成事件的批量更新,不管在哪里调用 setState,都自动批量

// ✅ React 18:所有场景都自动批量,只触发 1 次渲染
setTimeout(() => {
  setCount(1);
  setFlag(true);
  setName("yi");
}, 1000);
场景React 17React 18
React 事件处理器✅ 批量✅ 批量
setTimeout❌ 不批量✅ 批量
Promise 回调❌ 不批量✅ 批量
原生 DOM 事件❌ 不批量✅ 批量

3.4 flushSync:强制同步刷新

有时确实需要”立即渲染”——比如 setState 后立刻读取 DOM 尺寸。flushSync 是 escape hatch:

import { flushSync } from "react-dom";

function handleClick() {
  flushSync(() => {
    setCount(1); // 立即渲染
  });
  // 此时 DOM 已更新,可以安全读取
  const height = ref.current.offsetHeight;
}

⚠️ flushSync 会强制同步刷新整个组件树中待处理的更新,性能开销大,谨慎使用。

日常开发中,99% 的场景不需要 flushSync。自动批量就是最好的默认行为。

四、useTransition

4.1 问题:紧急更新被非紧急更新阻塞

经典场景:搜索框输入 + 列表过滤。

function SearchPage() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  function handleChange(e) {
    setQuery(e.target.value); // 更新输入框
    setResults(filterItems(e.target.value)); // 更新列表(可能很慢)
  }
  // ...
}

问题:setQuerysetResults同等优先级。列表过滤耗时,输入框也被拖慢——用户打字感觉卡顿。

4.2 useTransition:标记低优先级更新

const [isPending, startTransition] = useTransition();

改写搜索场景:

function SearchPage() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    // 紧急更新:输入框立即响应
    setQuery(e.target.value);

    // 非紧急更新:列表过滤可以延后
    startTransition(() => {
      setResults(filterItems(e.target.value));
    });
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <ResultList results={results} />
    </>
  );
}

类比:useTransition 像餐厅的”预订单”——你点了菜(状态更新),但告诉厨房”不急,先做别人的”(低优先级)。而正常下单(紧急更新)是”我现在就要”。

4.3 isPending 的用途

// 1. 显示加载指示器
{isPending && <Spinner />}

// 2. 降低列表视觉优先级
<div style={{ opacity: isPending ? 0.7 : 1 }}>
  <ResultList results={results} />
</div>

// 3. 禁用交互,避免过渡期间误操作
<button disabled={isPending}>提交</button>

4.4 startTransition 的注意事项

// ✅ 正确:在 startTransition 内调用 setState
startTransition(() => {
  setResults(newResults);
});

// ✅ 更好:把耗时计算也放进 startTransition
startTransition(() => {
  setResults(filterItems(query)); // 计算和更新都在低优先级
});

// ❌ 错误:不能在 startTransition 内使用 async/await
startTransition(async () => {
  const data = await fetchData(); // ❌ 不支持异步
  setResults(data);
});

// ✅ 正确:先获取数据,再标记过渡
const data = await fetchData();
startTransition(() => {
  setResults(data);
});

startTransition 只能包裹同步的状态更新。异步操作在 await 之后再标记为过渡。

五、useDeferredValue

5.1 延迟一个值的更新

const deferredValue = useDeferredValue(value);

value 变化时,deferredValue 不会立即更新——如果有更紧急的更新在排队,它会等紧急更新处理完再更新。

5.2 用 useDeferredValue 改写搜索场景

function SearchPage() {
  const [query, setQuery] = useState("");
  const deferredQuery = useDeferredValue(query); // 延迟版本的 query

  return (
    <>
      {/* 输入框绑定原始 query,立即响应 */}
      <input value={query} onChange={e => setQuery(e.target.value)} />

      {/* 列表使用延迟版本,不阻塞输入 */}
      <ResultList query={deferredQuery} />
    </>
  );
}

// ResultList 用 React.memo 包裹,deferredQuery 没变时不重渲染
const ResultList = React.memo(function ResultList({ query }) {
  const results = filterItems(query); // 可能耗时的过滤
  return (
    <ul>
      {results.map(item => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
});

关键点:ResultList 用了 React.memo——当 deferredQuery 没变时不重渲染。配合模式:延迟值 + memo

5.3 useDeferredValue vs useTransition

useTransitionuseDeferredValue
思路主动标记:我告诉你这个更新优先级低被动延迟:你帮我延迟这个值
控制粒度控制整个更新(可包含多个 setState)控制单个值
有无 isPending✅ 有❌ 没有,需自己判断
适用场景你控制 setState 的调用你只接收一个 prop/value
代码侵入性需修改状态更新逻辑只需在消费端加一行

5.4 什么时候用哪个

// 场景 1:你控制 setState → 用 useTransition
startTransition(() => {
  setResults(filterItems(query)); // 非紧急
});

// 场景 2:你只接收一个 prop → 用 useDeferredValue
const deferredQuery = useDeferredValue(query);

简单判断:能改 setState 的地方用 useTransition,只能改消费端的地方用 useDeferredValue

六、Suspense 与并发渲染

6.1 并发模式下的 Suspense

Suspense机制详解 中详细讲了 Suspense 的机制。这里只说一点:Suspense 在并发模式下才能真正发挥威力

React 17 的 Suspense 是”全有或全无”——组件加载中,整个区域显示 fallback。React 18 并发渲染让 Suspense 更智能:

<Suspense fallback={<Loading />}>
  <ProfileHeader /> {/* 已有数据,立即显示 */}
  <UserPosts userId={id} /> {/* 正在加载,显示 fallback */}
</Suspense>

6.2 Selective Hydration

SSR 场景下,React 18 引入了 Selective Hydration(选择性注水)按优先级逐步注水——用户正在交互的部分先注水,其他后注水。

<Layout>
  <Suspense fallback={<Skeleton />}>
    <Comments /> {/* 用户正在交互 → 优先注水 */}
  </Suspense>
  <Suspense fallback={<Skeleton />}>
    <Sidebar /> {/* 暂时不需要 → 延后注水 */}
  </Suspense>
</Layout>

类比:Selective Hydration 像酒店入住——前台先给你房卡(核心交互),其他权限慢慢开通。

七、实战:搜索框优化完整示例

7.1 优化前:输入卡顿

function App() {
  const [query, setQuery] = useState("");

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      {/* 10000 条数据过滤,每次输入都卡 */}
      <SlowList query={query} />
    </>
  );
}

7.2 方案 A:useTransition

function App() {
  const [inputValue, setInputValue] = useState("");
  const [searchQuery, setSearchQuery] = useState("");
  const [isPending, startTransition] = useTransition();

  return (
    <>
      <input
        value={inputValue}
        onChange={e => {
          setInputValue(e.target.value); // 紧急:输入框立即更新
          startTransition(() => {
            setSearchQuery(e.target.value); // 非紧急:列表延后更新
          });
        }}
      />
      {isPending && <Spinner />}
      <SlowList query={searchQuery} />
    </>
  );
}

7.3 方案 B:useDeferredValue

function App() {
  const [query, setQuery] = useState("");
  const deferredQuery = useDeferredValue(query);

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <SlowList query={deferredQuery} />
    </>
  );
}

const SlowList = React.memo(function SlowList({ query }) {
  const items = heavyFilter(query);
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
});

7.4 两种方案的选择

方案 A(useTransition)方案 B(useDeferredValue)
代码量稍多(两个 state)更少(一个 state + 一行 defer)
过渡状态✅ 有 isPending❌ 需自己比较 query !== deferredQuery
控制力更强(可包裹多个 setState)更弱(只能延迟一个值)
推荐场景需要过渡 UI 反馈只需延迟渲染

大多数搜索场景,useDeferredValue 更简洁。需要过渡状态指示器时,用 useTransition。

八、常见误区

8.1 “并发渲染让我的 App 变快了”

❌ 并发渲染不会让单次渲染更快。它做的是让紧急更新不被非紧急更新阻塞——用户感知更流畅,但总工作量没变。

✅ 正确理解:并发渲染让 App 响应更快,不是渲染更快。

8.2 “useTransition 里可以做异步操作”

startTransition 不支持 async 回调,只能包裹同步的 setState。

// ❌ 错误
startTransition(async () => {
  const data = await fetchResults(query);
  setResults(data);
});

// ✅ 正确
const data = await fetchResults(query);
startTransition(() => {
  setResults(data);
});

8.3 “所有 setState 都应该放进 startTransition”

❌ 只有非紧急的、用户不期望立即看到结果的更新才应该放进 startTransition。输入框的值、开关状态、表单提交反馈——这些都是紧急的。

✅ 判断标准:用户是否期望这个更新立即生效? 期望 → 紧急;不期望 → 可标记为过渡。

8.4 “useDeferredValue 会丢数据”

useDeferredValue 不会丢数据——它只是延迟更新。最终值一定会和原始值同步,只是中间可能短暂显示旧值。

✅ 延迟 ≠ 丢弃。就像慢一拍的回声——声音还在,只是晚了一点。

九、API 速查

useTransition

const [isPending, startTransition] = useTransition();

// isPending: boolean — 是否有过渡更新在进行
// startTransition: (callback: () => void) => void — 标记回调中的更新为低优先级

useDeferredValue

const deferredValue = useDeferredValue(value);

// value: 任意类型 — 需要延迟的值
// deferredValue: 与 value 同类型 — 延迟版本的值
// 配合 React.memo 使用效果最佳

flushSync

import { flushSync } from "react-dom";

flushSync(() => {
  setState(newValue);
});
// 强制同步刷新,立即完成 DOM 更新
// ⚠️ 性能开销大,谨慎使用

十、总结

一句话:React 18 并发渲染 = Fiber 的中断能力 + 优先级调度 + 用户层 API(useTransition / useDeferredValue),让紧急更新不被非紧急更新拖累。

核心要点:

  1. 并发渲染:渲染可中断、可恢复、可放弃,仍是单线程
  2. Fiber 是基础设施,并发特性是上层 API——Fiber 提供能力,React 18 提供调度
  3. Automatic Batching:所有场景自动批量,flushSync 是 escape hatch
  4. useTransition:主动标记低优先级更新,有 isPending 状态
  5. useDeferredValue:被动延迟值的更新,配合 memo 使用
  6. Suspense + 并发:已有内容不被新加载阻塞,Selective Hydration 按优先级注水

相关


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
React
第 14 / 17 篇
查看系列全部文章
  1. 01.React 核心(1):React 组件与函数的本质区别
  2. 02.React 核心(2):声明式与 React 的实现机制
  3. 03.React 核心(3):Props 完全指南
  4. 04.React 核心(4):useState 完全指南
  5. 05.React 核心(5):useEffect 完全指南
  6. 06.React 核心(6):useRef 完全指南
  7. 07.React 核心(7):Context API 与状态共享
  8. 08.React 核心(8):useCallback 与 useMemo
  9. 09.React 核心(9):Hooks 规则与闭包陷阱
  10. 10.React 核心(10):自定义 Hook 设计
  11. 11.React 核心(11):Key 与列表渲染
  12. 12.React 核心(12):表单与受控/非受控组件
  13. 13.React 核心(13):React 渲染机制深入——Fiber 与调度
  14. 14.React 核心(14):React 18 并发特性
  15. 15.React 核心(15):Suspense 机制详解
  16. 16.React 核心(16):TanStack Query 入门指南
  17. 17.React 核心(17):TanStack Query 缓存机制详解

上一篇
React 核心(15):Suspense 机制详解
下一篇
React 核心(13):React 渲染机制深入——Fiber 与调度