Skip to main content

React 18 / 19 新特性

特性说明
并发渲染(Concurrent)渲染可中断、可恢复、可丢弃,是下面所有能力的底层基础
自动批处理(Automatic Batching)setTimeout/Promise/原生事件里的多次 setState 也会合并为一次渲染
useTransition / startTransition把非紧急更新标记为过渡任务,可被高优先级交互打断
useDeferredValue延迟某个值的更新,常用于搜索联想等场景
useId生成 SSR 前后一致的唯一 id,避免 hydration 不匹配
useSyncExternalStore供状态库订阅外部源,解决并发下的撕裂
createRoot新的根 API,启用并发特性(取代 ReactDOM.render
Suspense + SSR 流式渲染支持 renderToPipeableStream、选择性 hydration

面试题:为什么 React 18 要把同步渲染改成并发? 同步渲染一旦开始无法中断,大组件树渲染会阻塞用户输入。并发渲染让 React 能在渲染过程中「让位」给高优先级任务(如打字、点击),渲染完再继续,从而保证交互流畅。

React 18 核心更新 (Concurrent Features)

一文解读 React 17 与 React 18 的更新变化

1.自动批量更新 (Automatic Batching)

在 React 18 之前,只有在 React 的合成事件(如 onClick)中才会批量更新 setState。而在 setTimeout、Promise 或原生事件中连续多次 setState,会导致多次 render。 React 18:无论在哪里(包括 setTimeout 和异步回调),只要处于同一个宏任务或微任务的执行栈中,多次 setState 都会被合并为一次更新。

原理解析:为什么 React 18 能做到全场景的自动批处理?

  • React 17 时代:React 依赖内部的一个全局变量 isBatchingUpdates。只有在进入 React 自身的合成事件回调前,才会把这个变量设为 true。但如果你的 setState 写在了 setTimeout 里面,等定时器触发时,外层的同步事件代码早就跑完了,isBatchingUpdates 已经恢复成了 false,所以后续每个 setState 都会立即触发一次同步渲染。
  • React 18 时代:抛弃了通过全局变量强行包裹的方式,转而引入了全新的 Lane(车道)优先级调度机制。当你调用 setState 时,React 不再立刻去渲染,而是给这次更新分配一个优先级,并向浏览器的微任务队列 (Microtask Queue) 中注册一个调度任务。如果你在同一个 setTimeout 里连续调了 3 次 setState,React 只是把 3 个更新任务塞进了同一个队列(赋予相同的优先级)。等这轮宏任务里的同步代码跑完,JS 引擎去清空微任务队列时,React 才会把这 3 个状态合并,统一执行一次渲染。

2.Concurrent Mode (并发模式)

这是 React 18 最重大的底层架构变更。React 可以在后台准备新的屏幕内容而不阻塞主线程。

Q:React 是默认开启并发的吗?

答:是,也不是。 只要你使用了 createRoot,React 内部的 Lane 调度模型就激活了。但 React 非常克制,它遵循“默认同步,按需并发”的原则。如果你不使用 useTransition 等 Hook,大部分更新(如点击事件里的 setState)依然表现得像同步渲染一样,以保证响应的即时性。只有当你显式使用了并发 Hook,React 才会开启“可中断”的模式。

⚛️ React 内部的优先级任务预设 (Lane Model)

React 内部将任务划分为多个 Lane(车道),常见的优先级排序如下:

  1. Immediate Lane (同步优先级)
    • 触发场景:离散交互(如 click, input, keydown)。
    • 表现:必须立即执行,不能被中断,否则用户会感到“粘手”。
  2. Continuous Lane (连续优先级)
    • 触发场景:连续交互(如 scroll, drag, mouseenter)。
    • 表现:优先级略低于离散交互,但仍需快速响应。
  3. Default Lane (默认优先级)
    • 触发场景:异步请求返回后的数据更新、setTimeout 等。
  4. Transition Lane (过渡优先级)
    • 触发场景:useTransition / useDeferredValue
    • 表现:优先级最低,可以被任何高优先级任务打断。这就是为什么它们能解决大列表渲染卡顿的原因。
  5. Idle Lane (空闲优先级)
    • 触发场景:离屏渲染(Offscreen)等。

🔄 useTransition vs useDeferredValue

这俩兄弟的优先级是一样的(都属于 Transition Lane),区别在于“切入点”不同:

  • useTransition (动作导向)
    • 用法const [isPending, startTransition] = useTransition()
    • 场景:你明确知道某个动作(如点击 Tab、点击搜索按钮)会导致大面积渲染,你用它包裹那个 setState
  • useDeferredValue (数据导向)
    • 用法const deferredValue = useDeferredValue(value)
    • 场景:你拿不到产生数据的那个动作(比如数据是从父组件 Props 传下来的),你只能选择“延迟”这个的更新。

3.其他新特性

  • useDeferredValue:与 useTransition 类似,用于延迟某个非紧急值的更新。

  • Suspense 真正支持了 SSR 流式渲染。

  • useSyncExternalStore (解决并发下的撕裂): 这是 React 18 为状态管理库(如 Redux, Zustand)设计的 Hook。

    为什么需要它?—— 什么是“撕裂 (Tearing)”?

    在并发模式下,渲染是可以中断的。假设一个组件在渲染到一半时,外部存储(比如 Redux 中的状态)发生了更新,此时 React 恢复渲染,会导致同一个渲染周期内,不同的组件读取到了不同版本的状态。

    直观类比:你在画一张合影,画到一半时模特换了件衣服,结果画出来的合影里,有的人穿夏装,有的人穿冬装,这就叫“撕裂”。

    实际工作中什么场景会用到?

    1. 状态库开发者:如果你在写类似 Redux 或 Zustand 的库,必须用它来确保在并发渲染下状态的一致性。
    2. 订阅外部系统源:比如订阅浏览器的 window.innerWidth、网络状态 navigator.onLine 或地理位置。
    // 例子:实时监听窗口宽度(并发安全版)
    function useWindowWidth() {
    return useSyncExternalStore(
    // subscribe:订阅函数,当数据变动时触发回调
    (callback) => {
    window.addEventListener("resize", callback);
    return () => window.removeEventListener("resize", callback);
    },
    // getSnapshot:获取当前数据的快照
    () => window.innerWidth,
    // getServerSnapshot:SSR 时的快照(可选)
    () => 0,
    );
    }

    注意:普通业务开发建议优先使用 useState + useEffect,只有在处理非 React 管理的外部数据源且需要高并发渲染安全时,才考虑 useSyncExternalStore

    Q:那为啥不在原有的 Context API 上改良,非要搞个外部 Store?

    答:这是一个非常深刻的工程权衡问题,核心原因有三点:

    1. 内部 vs 外部 (Ownership)
      • Context 是 React 内部的状态。React 完全掌控它的更新时机,它本身就不会“撕裂”,因为它和 Fiber 树是一体的。
      • 外部 Store(Redux/Zustand)是在 React 之外的。它们可能在任何时候(比如一个 WebSocket 回调里)更新。React 无法阻止外部数据的变动,所以需要一个“桥梁”来同步。
    2. 性能瓶颈 (The Selector Problem)
      • Context 是“全量推送”模型。一旦 Provider 的 value 变了,所有用到该 Context 的组件都会强制重渲染。
      • 外部 Store 配合 useSyncExternalStore 可以实现“按需订阅”(通过 Selector)。只有你关心的那部分数据变了,组件才会动。如果强行用 Context 做全局状态管理,大项目会卡得没法用。
    3. 并发下的确定性
      • useSyncExternalStore 的名字里带个 Sync。它的底层逻辑是:如果在渲染过程中发现外部数据变了,它会立即丢弃当前的并发渲染结果,强制发起一次同步渲染,以确保用户看到的 UI 永远是数据的一致快照。这是 Context 这种内置机制无法(也没必要)提供的特殊保障。

    总结:Context 的定位是“低频、跨层级传递数据”(如主题、语言);而 useSyncExternalStore 是为了让 React 能安全地接入“高频、复杂的外部状态管理系统”。


React 19 新特性(重点)

截至 2026 年 React 19 已是主流版本,是当前面试的热点。

1. Actions 与表单

把「数据变更 + pending 状态 + 错误处理 + 乐观更新」标准化。表单的 action 可以直接传一个(异步)函数。

  • useActionState(原 useFormState):管理 action 的状态、返回值与 pending。
const [error, submitAction, isPending] = useActionState(
async (prevState, formData) => {
const err = await updateName(formData.get("name"));
if (err) return err;
redirect("/profile");
return null;
},
null,
);

return (
<form action={submitAction}>
<input name="name" />
<button disabled={isPending}>提交</button>
</form>
);
  • useFormStatus:在表单子组件里直接读取父 <form> 的提交状态(无需层层传 props)。

2. useOptimistic(乐观更新)

在异步请求返回前先「乐观地」更新 UI,请求失败/完成后再以真实结果为准。

const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(state, newMsg) => [...state, { text: newMsg, sending: true }],
);

3. use API

可在渲染中读取 Promise 或 Context,配合 Suspense 实现更顺滑的数据读取;与其他 Hook 不同,use 可以在条件语句里调用。

function Comment({ commentPromise }) {
const comment = use(commentPromise); // 会触发最近的 Suspense
return <p>{comment}</p>;
}

4. 其他改进

  • ref 作为普通 prop:函数组件可直接接收 ref,不再强制 forwardRef
  • <Context> 直接当 Provider<ThemeContext value={...}>,无需 .Provider
  • 文档元数据:组件内直接写 <title>/<meta>/<link>,React 会自动提升到 <head>
  • 资源预加载preload/preinit 等 API。
  • 改进的 hydration 错误提示

React Server Components(RSC)

面试题:什么是 Server Components?解决什么问题?

  • 服务端组件:只在服务端运行、不打包进客户端 bundle 的组件,可直接访问数据库/文件系统,零客户端 JS 体积,对首屏和 SEO 友好。
  • Client Components"use client" 标记,可用状态和事件)配合使用。
  • 是 Next.js App Router 的核心模型;面试常问「RSC 与 SSR 的区别」:SSR 是把组件渲染成 HTML 字符串后再 hydration,组件代码仍会下发到客户端;RSC 的服务端组件代码完全不下发,只传渲染结果(特殊序列化格式)。