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(车道),常见的优先级排序如下:
- Immediate Lane (同步优先级):
- 触发场景:离散交互(如
click,input,keydown)。 - 表现:必须立即执行,不能被中断,否则用户会感到“粘手”。
- 触发场景:离散交互(如
- Continuous Lane (连续优先级):
- 触发场景:连续交互(如
scroll,drag,mouseenter)。 - 表现:优先级略低于离散交互,但仍需快速响应。
- 触发场景:连续交互(如
- Default Lane (默认优先级):
- 触发场景:异步请求返回后的数据更新、
setTimeout等。
- 触发场景:异步请求返回后的数据更新、
- Transition Lane (过渡优先级):
- 触发场景:
useTransition/useDeferredValue。 - 表现:优先级最低,可以被任何高优先级任务打断。这就是为什么它们能解决大列表渲染卡顿的原因。
- 触发场景:
- 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 恢复渲染,会导致同一个渲染周期内,不同的组件读取到了不同版本的状态。
直观类比:你在画一张合影,画到一半时模特换了件衣服,结果画出来的合影里,有的人穿夏装,有的人穿冬装,这就叫“撕裂”。
实际工作中什么场景会用到?
- 状态库开发者:如果你在写类似 Redux 或 Zustand 的库,必须用它来确保在并发渲染下状态的一致性。
- 订阅外部系统源:比如订阅浏览器的
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?
答:这是一个非常深刻的工程权衡问题,核心原因有三点:
- 内部 vs 外部 (Ownership):
- Context 是 React 内部的状态。React 完全掌控它的更新时机,它本身就不会“撕裂”,因为它和 Fiber 树是一体的。
- 外部 Store(Redux/Zustand)是在 React 之外的。它们可能在任何时候(比如一个 WebSocket 回调里)更新。React 无法阻止外部数据的变动,所以需要一个“桥梁”来同步。
- 性能瓶颈 (The Selector Problem):
- Context 是“全量推送”模型。一旦 Provider 的
value变了,所有用到该 Context 的组件都会强制重渲染。 - 外部 Store 配合
useSyncExternalStore可以实现“按需订阅”(通过 Selector)。只有你关心的那部分数据变了,组件才会动。如果强行用 Context 做全局状态管理,大项目会卡得没法用。
- Context 是“全量推送”模型。一旦 Provider 的
- 并发下的确定性:
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 的服务端组件代码完全不下发,只传渲染结果(特殊序列化格式)。