11.7 React 并发特性:Concurrent Mode、Lane 模型、Time Slicing、Suspense

Concurrent Mode、Lane 模型、Time Slicing、Suspense

Concurrent ModeSuspenseLaneTime Slicing

原理

React 18 正式引入了并发特性(Concurrent Features),这是一套底层架构能力,允许 React 同时准备多个版本的 UI,并根据优先级决定何时展示哪个版本。它不是可选模式,而是 React 18 的默认行为。

Time Slicing(时间切片)

Time Slicing 是并发特性的基础能力。它将大型渲染任务拆分为多个小片(chunk),在每个时间片(通常 5ms 左右)后让出主线程,检查是否有更高优先级的任务(如用户输入)需要处理。

实现机制:

  • React 使用 MessageChannel(或 setTimeout 降级)实现宏任务调度。
  • 每个 Fiber 工作单元执行后检查是否超时(shouldYield())。
  • 若超时或存在更高优先级更新,保存当前进度,让出主线程。

这使得 React 应用即使在处理大量数据时,也能保持对用户输入的响应。

Lane 模型

React 18 使用 Lane 模型替代了之前的 Expiration Time 模型来管理更新优先级。Lane 使用位掩码(bitmask)表示,每个位代表一种更新类型:

  • SyncLane:同步更新,最高优先级(如 React DOM 事件处理)。
  • InputContinuousLane:连续输入(如 onMouseMoveonChange)。
  • DefaultLane:默认更新(如 setState)。
  • TransitionLane:过渡更新(如 startTransition 包裹的更新)。
  • IdleLane:空闲时执行的更新。

Lane 模型的优势:

  • 优先级比较通过位运算实现,效率极高。
  • 多个 Lane 可以合并(按位或),也可以拆分(按位与)。
  • 支持批量处理同一优先级的多个更新。

startTransition

startTransition 是 React 18 提供的显式 API,用于标记非紧急更新:

import { startTransition } from 'react';

function handleChange(e) {
  const value = e.target.value;
  // 紧急更新:输入框必须立即响应
  setInputValue(value);

  // 非紧急更新:搜索结果可以延迟
  startTransition(() => {
    setSearchResults(filterLargeList(value));
  });
}

startTransition 包裹的更新会被分配较低的 TransitionLane 优先级。如果期间发生更高优先级的更新(如继续输入),React 会中断过渡更新,优先处理紧急更新。

useTransition Hook

useTransition 提供了 startTransition 函数和 isPending 状态:

const [isPending, startTransition] = useTransition();

return (
  <>
    {isPending && <Spinner />}
    <Results data={results} />
  </>
);

Suspense 与数据获取

Suspense 允许组件在"等待"某些内容(如异步数据、懒加载组件、图片)时显示 fallback UI:

import { Suspense } from 'react';

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <ProfileData />
    </Suspense>
  );
}

Suspense 的工作机制

  1. 组件在渲染时抛出 Promise(或从缓存中读取未就绪的数据)。
  2. React 捕获该 Promise,暂停该子树的渲染。
  3. 显示最近的 Suspense fallback。
  4. Promise 解决后,React 恢复渲染子树。

React 18 扩展了 Suspense 的能力:

  • Suspense 与 Transition 结合startTransition 中的 Suspense 不会立即显示 fallback,而是保留旧 UI 直到新数据就绪(避免加载状态闪烁)。
  • SuspenseList(实验性):控制多个 Suspense 边界的显示顺序。

useDeferredValue

useDeferredValue 用于延迟更新某个值,类似于防抖但由 React 调度:

const deferredQuery = useDeferredValue(query);

// query 变化时,先使用旧 deferredQuery 渲染(保持响应)
// 然后在空闲时更新为新的 deferredQuery
const results = useMemo(() => search(deferredQuery), [deferredQuery]);

startTransition 的区别:useDeferredValue 延迟的是值,而 startTransition 延迟的是状态更新。

并发特性不是并发渲染

React 的"并发"并非指多线程并行渲染(JavaScript 是单线程的),而是指 React 可以在内存中同时准备多个版本的 UI(多棵树),并根据优先级决定提交哪一个。这是一种"可中断的渲染",而非真正的并行计算。

用法

// 结合 Suspense 和 startTransition 实现流畅的 Tab 切换
function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  const selectTab = (nextTab) => {
    startTransition(() => {
      setTab(nextTab);
    });
  };

  return (
    <>
      <TabList activeTab={tab} onSelect={selectTab} />
      {isPending && <TabSkeleton />}
      <Suspense fallback={<TabSkeleton />}>
        <TabContent tab={tab} />
      </Suspense>
    </>
  );
}

实践

数据获取模式演进

React 18 推荐的 Suspense 数据获取模式:

  1. Fetch-on-Render:组件挂载后发起请求(传统 useEffect 模式)。
  2. Fetch-Then-Render:在路由或父组件中预取数据,再渲染。
  3. Render-as-You-Fetch:开始渲染后立即发起请求,Suspense 处理等待状态(推荐)。

框架如 Next.js App Router、Remix 已内置支持 Render-as-You-Fetch 模式。

并发模式下的状态一致性

在并发模式下,一个组件可能在渲染过程中被多次调用(因为高优先级更新打断了低优先级更新)。因此,Render Phase 必须是纯函数:

  • 不要在 render 中修改外部变量。
  • 不要在 render 中发起网络请求。
  • 使用 useMemo 缓存计算,但确保计算是纯的。

严格模式的双重调用

React 18 的严格模式在开发环境下会故意双重调用某些函数(如渲染函数、某些 effect 的清理和重新执行),以帮助检测副作用。这是为了模拟并发模式下组件可能被多次调用的情况。

陷阱

| 陷阱 | 现象 | 解决方案 | |------|------|---------| | startTransition 中执行同步 setState | 过渡效果失效 | 将紧急更新放在 startTransition 外 | | Suspense 边界放置不当 | 整个页面闪烁 fallback | 将 Suspense 放在更细粒度的组件边界 | | 在 render 中创建 Promise | 无限循环渲染 | 使用数据缓存层(如 React Query、SWR) | | 并发模式下的全局变量修改 | 状态不一致、难以复现的 Bug | 将状态放入 React state 或 ref | | 忽略 useDeferredValue 的依赖 | 延迟值未更新 | 确保传入的值变化会触发延迟更新 |

测验

关联章节网络

当前章节
关联章节
交叉引用
后续延伸