React
Web 与框架⭐⭐ 中级🔥 高频
💡 核心要点
React 是 Facebook 开源的声明式 UI 库,核心哲学是 UI = f(state):给定相同的状态,始终渲染相同的界面。现代 React 以函数组件 + Hooks 为主流,通过虚拟 DOM 和 Fiber 架构实现高效的 UI 更新。掌握 Hooks、虚拟 DOM 原理和性能优化是面试核心考点。
版本演进时间线
理解 React 的版本演进,本质是看清"架构换代"和"心智模型迁移"两条主线。每一次大版本都对应一次开发模式的范式转移。
| 版本 | 发布时间 | 关键变化 | 心智模型影响 |
|---|---|---|---|
| 0.3 – 0.14 | 2013.05 – 2015.10 | 首次开源(JSConf US 2013);JSX、虚拟 DOM、单向数据流 | 声明式 UI 替代 jQuery 命令式操作 |
| 15 | 2016.04 | 重写核心,DOM 渲染从 SVG 命名空间剥离;正式版本号 1.0 之外的成熟 | 稳定 API,社区开始大规模采用 |
| 16(Fiber) | 2017.09 | Fiber 架构重写:可中断渲染、错误边界、Portals、Fragment、自定义 DOM 属性 | 协调器从递归改为可中断链表,为并发模式埋下基础 |
| 16.3 | 2018.03 | 新生命周期(getDerivedStateFromProps)、新 Context API、React.forwardRef | 旧 componentWillMount/Receive/Update 被标记 Unsafe |
| 16.6 | 2018.10 | React.memo、React.lazy + Suspense(仅代码分割) | 懒加载首次正式登场 |
| 16.8(Hooks) | 2019.02 | 🔥 Hooks 正式发布:useState / useEffect / useContext / useReducer | 函数组件取代类组件成为默认;逻辑复用从 HOC/Render Props 转向自定义 Hook |
| 17 | 2020.10 | "无新特性"版本:新 JSX Transform(无需 import React)、事件委托从 document 改为 root | 为多版本共存铺路,被称为"过渡桥梁" |
| 18 | 2022.03 | 并发渲染(Concurrent Rendering)正式 GA:createRoot、useTransition、useDeferredValue、useId、Automatic Batching、Suspense for SSR、Streaming SSR | 渲染从同步到可打断;状态更新可标记优先级 |
| 18.2 / 18.3 | 2022.06 – 2024.04 | 稳定期;19 升级前的最后过渡版(useFormState 等实验性) | 生态全面拥抱并发模式 |
| 19 | 2024.12 | 🔥 Actions / useActionState / useOptimistic / useFormStatus;ref as prop(不再需要 forwardRef);use() API(条件读取 Promise/Context);Document Metadata(<title> / <meta> 直接放组件内);Server Components 稳定 API;<form action> 直接传函数 | 表单状态托管给框架;元数据组件化;客户端/服务端边界清晰化 |
| 19.1 | 2025.03 | Owner Stack 调试增强;服务端错误更易追踪 | 调试体验向 Server Components 靠拢 |
| React Compiler RC | 2025 年 | 🔥 自动 memo 化:编译器静态分析,自动插入 useMemo / useCallback / React.memo | 性能优化从"手动 memo"走向"零心智负担" |
⚠️ 版本迁移高频面试题
- "为什么 React 17 没有新特性?" → 不是没做事,而是把所有破坏性改动延迟,让企业能在同一应用中混用 17/18 子树渐进升级。
- "
createRoot和ReactDOM.render的区别?" → 18 用createRoot才开启并发模式;老 API 仍可用但性能与行为按旧逻辑走。 - "React 19 是不是要废掉 Redux?" → 不会废,但 Server Actions +
useActionState+useOptimistic让大量 Redux 用于"表单 + 加载状态"的场景变得多余;Redux 仍适合复杂客户端状态机。 - "React Compiler 出来后还要写
useMemo吗?" → 默认不需要;但 Compiler 不接管副作用、不能跨组件优化、对动态依赖无能为力,仍需理解原理以应对边缘场景。
三大阶段心智模型
2013 ─────────── 2019 ─────────── 2022 ─────────── 2024+
"虚拟 DOM 时代" "Hooks 时代" "并发渲染时代" "Server/Compiler 时代"
(Concurrent) (RSC + Auto-Memo)
React 0.x-15 React 16.8-17 React 18 React 19+
类组件主流 函数组件 + Hooks 可中断渲染 服务端组件 + 编译器优化
生命周期为王 自定义 Hook 复用 优先级调度 客户端代码减少面试黄金答法:被问到"React 这些年发展脉络"时,按这四个阶段讲,比按版本号逐个背书更有结构感。
核心概念
1. 组件化思想
React 的核心哲学:UI = f(state)。给定相同输入(props + state),组件总是渲染相同的输出。
函数组件 vs 类组件
现代 React(16.8+)推荐函数组件 + Hooks,类组件逐渐退出历史舞台。
// 函数组件(推荐)
function Greeting({ name }: { name: string }) {
const [count, setCount] = useState(0)
return (
<div>
<p>Hello, {name}!</p>
<button onClick={() => setCount(c => c + 1)}>点击 {count} 次</button>
</div>
)
}
// 类组件(了解即可)
class GreetingClass extends React.Component<{ name: string }, { count: number }> {
state = { count: 0 }
render() {
return (
<div>
<p>Hello, {this.props.name}!</p>
<button onClick={() => this.setState(s => ({ count: s.count + 1 }))}>
点击 {this.state.count} 次
</button>
</div>
)
}
}| 对比项 | 函数组件 | 类组件 |
|---|---|---|
| 语法 | 简洁,纯函数风格 | 需要继承 React.Component |
| 状态管理 | useState Hook | this.state + setState |
| 生命周期 | useEffect 模拟 | 显式生命周期方法 |
| 逻辑复用 | 自定义 Hook(优雅) | HOC / Render Props(复杂) |
this 问题 | 无 | 需要手动绑定 |
| 性能 | 更易优化(无实例化开销) | 相对较重 |
JSX 本质
JSX 是 React.createElement() 的语法糖,编译后变成普通 JS 对象:
// JSX 写法
const element = <h1 className="title">Hello</h1>
// 编译后等价于(React 17+ 用新 JSX Transform,不需要手动 import React)
const element = React.createElement('h1', { className: 'title' }, 'Hello')
// 最终生成的 JS 对象(虚拟 DOM 节点)
const element = {
type: 'h1',
props: { className: 'title', children: 'Hello' },
key: null,
ref: null,
}Props vs State
| 对比项 | Props | State |
|---|---|---|
| 来源 | 父组件传入 | 组件内部管理 |
| 是否可变 | 只读(不可直接修改) | 可通过 setter 修改 |
| 修改触发 | 父组件重渲染 | setState / useState setter |
| 典型场景 | 配置、数据传入 | 用户交互、内部数据 |
2. 虚拟 DOM 与 Diff 算法
虚拟 DOM 是什么
虚拟 DOM(Virtual DOM)是用 JavaScript 对象描述真实 DOM 结构的轻量级树。React 维护两棵虚拟 DOM 树:当前树(current)和新树(workInProgress),通过对比两棵树的差异来最小化真实 DOM 操作。
状态变化 → 生成新的虚拟 DOM → Diff 比较 → 计算最小变更 → 更新真实 DOM为什么需要虚拟 DOM
直接操作真实 DOM 的问题:
- DOM 操作触发回流(reflow)和重绘(repaint),代价高昂
- 频繁小粒度的 DOM 操作性能差
- 虚拟 DOM 可批量合并操作,减少真实 DOM 访问次数
注意:虚拟 DOM 并非"快"的银弹,其真正价值是提供了声明式编程模型和跨平台能力(React Native)。
Diff 策略
React 的 Diff 算法基于三个假设(启发式算法,将 O(n³) 降低到 O(n)):
- 同层比较:不跨层级移动节点,只比较同一层的节点
- 类型决定复用:节点类型不同直接销毁重建(
div→span) - key 标识身份:有 key 时通过 key 识别节点,实现高效复用
// 没有 key:顺序比较,增删导致大量无效更新
// 有 key:通过 key 精准匹配节点,减少 DOM 操作
const list = items.map(item => (
<li key={item.id}>{item.name}</li> // ✅ 使用稳定唯一 id 作为 key
))Fiber 架构
React 16 引入 Fiber 重写了协调(Reconciliation)引擎:
- 旧架构(Stack Reconciler):递归处理,一旦开始不可中断,大树更新会阻塞主线程
- Fiber 架构:将渲染工作拆成小单元(fiber 节点),利用浏览器空闲时间(
requestIdleCallback思想)分片执行,支持中断、恢复、优先级调度 - 分两个阶段:
- Render 阶段(可中断):构建 Fiber 树,计算变更
- Commit 阶段(不可中断):将变更一次性应用到真实 DOM
3. React Hooks(重点)
Hooks 是 React 16.8 引入的特性,让函数组件拥有状态和生命周期能力。
useState — 状态管理
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
const [user, setUser] = useState<{ name: string } | null>(null)
// 基于当前值更新(推荐用函数形式,避免闭包陷阱)
const increment = () => setCount(prev => prev + 1)
// 更新对象状态:必须返回新对象,不能直接修改
const updateUser = () => setUser({ name: 'Alice' })
return <button onClick={increment}>Count: {count}</button>
}关键点: setState 不会直接修改当前 state,而是触发重渲染,在下一次渲染中使用新值。React 18 中 setState 默认批量更新(Automatic Batching)。
useEffect — 副作用
替代类组件的 componentDidMount、componentDidUpdate、componentWillUnmount:
import { useEffect, useState } from 'react'
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState(null)
useEffect(() => {
// 副作用:数据请求
let cancelled = false
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => {
if (!cancelled) setUser(data) // 防止组件卸载后 setState
})
// cleanup 函数:组件卸载或依赖变化前执行
return () => { cancelled = true }
}, [userId]) // 依赖数组:userId 变化时重新执行
return <div>{user?.name}</div>
}| 依赖数组 | 执行时机 |
|---|---|
| 不传 | 每次渲染后都执行(慎用) |
[] 空数组 | 仅在组件挂载后执行一次 |
[dep1, dep2] | 挂载后执行,dep 变化时重新执行 |
useRef — 持久引用
import { useRef, useEffect } from 'react'
function FocusInput() {
// 1. 访问 DOM 节点
const inputRef = useRef<HTMLInputElement>(null)
// 2. 存储不触发重渲染的可变值(如计时器 id)
const timerRef = useRef<number | null>(null)
useEffect(() => {
inputRef.current?.focus() // 组件挂载后自动聚焦
timerRef.current = window.setInterval(() => {
console.log('tick')
}, 1000)
return () => {
if (timerRef.current) clearInterval(timerRef.current)
}
}, [])
return <input ref={inputRef} />
}关键区别: useRef 的值变化不会触发重渲染,适合存储计时器、上一次的 props 值等。
useMemo / useCallback — 性能优化
import { useMemo, useCallback, useState } from 'react'
function ExpensiveComponent({ items, onItemClick }: {
items: number[]
onItemClick: (id: number) => void
}) {
// useMemo:缓存计算结果,items 不变则不重新计算
const total = useMemo(() => {
console.log('重新计算 total')
return items.reduce((sum, item) => sum + item, 0)
}, [items])
// useCallback:缓存函数引用,避免子组件不必要的重渲染
const handleClick = useCallback((id: number) => {
onItemClick(id)
}, [onItemClick])
return <div>Total: {total}</div>
}| Hook | 缓存对象 | 适用场景 |
|---|---|---|
useMemo | 计算结果(值) | 昂贵的计算逻辑 |
useCallback | 函数引用 | 传给子组件的回调,配合 React.memo 使用 |
useContext — 跨组件状态共享
import { createContext, useContext, useState } from 'react'
// 1. 创建 Context
const ThemeContext = createContext<'light' | 'dark'>('light')
// 2. Provider 提供值
function App() {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
return (
<ThemeContext.Provider value={theme}>
<DeepChild />
</ThemeContext.Provider>
)
}
// 3. 任意子孙组件消费,无需逐层传 props
function DeepChild() {
const theme = useContext(ThemeContext)
return <div className={`theme-${theme}`}>主题:{theme}</div>
}自定义 Hook — 逻辑复用
自定义 Hook 是以 use 开头的函数,可以调用其他 Hooks,实现逻辑的跨组件复用:
// 自定义 Hook:封装数据请求逻辑
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
setLoading(true)
fetch(url)
.then(r => r.json())
.then(d => { setData(d); setLoading(false) })
.catch(e => { setError(e); setLoading(false) })
}, [url])
return { data, loading, error }
}
// 复用
function UserList() {
const { data, loading } = useFetch<User[]>('/api/users')
if (loading) return <div>加载中...</div>
return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}Hooks 规则
- 只在顶层调用 Hook:不能在条件、循环或嵌套函数中调用(因为 React 依靠调用顺序识别 Hook)
- 只在函数组件或自定义 Hook 中调用:不能在普通 JS 函数中调用
// ❌ 错误:条件语句中使用 Hook
function Bad({ condition }: { condition: boolean }) {
if (condition) {
const [state, setState] = useState(0) // 违反规则!
}
}
// ✅ 正确:Hook 始终在顶层
function Good({ condition }: { condition: boolean }) {
const [state, setState] = useState(0)
// 条件可以在 Hook 内部处理
useEffect(() => {
if (condition) { /* ... */ }
}, [condition])
}4. 状态管理
状态分层原则
组件内状态 (useState)
↓ 多个组件共享,提升到父组件(状态提升)
父组件 + props 传递
↓ 跨多层组件
Context API(适合低频更新:主题、语言、用户信息)
↓ 状态复杂、更新频繁、组件间通信复杂
外部状态管理库(Redux、Zustand、MobX 等)状态管理库对比
| 库 | 核心思想 | 复杂度 | 适用场景 |
|---|---|---|---|
| Redux | 单一数据源、纯函数 Reducer、严格单向数据流 | 高(样板代码多) | 大型应用、需要时间旅行调试 |
| Redux Toolkit | Redux 官方推荐方案,大幅减少样板代码 | 中 | Redux 现代用法 |
| Zustand | 极简 store,无 Provider,直接订阅 | 低 | 中小型应用,快速上手 |
| MobX | 响应式编程,自动追踪依赖 | 中 | 习惯 OOP 风格的团队 |
| Recoil | 原子化状态,Facebook 出品 | 中 | 细粒度状态订阅 |
| Jotai | 原子化状态,比 Recoil 更轻量 | 低 | 原子化状态管理 |
// Zustand 示例(极简)
import { create } from 'zustand'
interface CountStore {
count: number
increment: () => void
}
const useCountStore = create<CountStore>(set => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
}))
function Counter() {
const { count, increment } = useCountStore()
return <button onClick={increment}>{count}</button>
}2026 状态管理决策树(必背)
💡 一句话定位
"先问数据来源"——服务端数据 vs 客户端状态是不同的问题,混着用就是踩坑。
┌──────────────────────────────────────────────────┐
│ Q1: 这个状态本质上是服务端数据吗? │
│ 是 → TanStack Query / SWR / RTK Query │
│ (缓存、失效、轮询、乐观更新都内置) │
│ 否 ↓ │
├──────────────────────────────────────────────────┤
│ Q2: 这个状态只在一个组件内用吗? │
│ 是 → useState / useReducer │
│ 否 ↓ │
├──────────────────────────────────────────────────┤
│ Q3: 只是配置型/低频更新(主题/语言/用户)吗? │
│ 是 → Context API(避免 prop drilling 已足够) │
│ 否 ↓ │
├──────────────────────────────────────────────────┤
│ Q4: 复杂、跨组件、需要中间件 / 时间旅行? │
│ 是 → Redux Toolkit / Zustand │
│ 否(细粒度原子状态)→ Jotai │
└──────────────────────────────────────────────────┘| 工具 | 类别 | 2026 趋势 | 适合 |
|---|---|---|---|
| TanStack Query (React Query) | 服务端状态 | 🔥🔥🔥 事实标准 | 任何带后端的应用 |
| SWR | 服务端状态 | 🔥🔥 Vercel 出品 | Next.js 项目 |
| Zustand | 客户端状态 | 🔥🔥🔥 | 大多数新项目首选 |
| Jotai | 客户端状态(原子) | 🔥🔥 | 复杂派生状态 |
| Redux Toolkit | 客户端状态 | 🔥🔥 | 大型企业项目、需要 Redux DevTools |
| MobX | 客户端状态 | 🔥 | OOP 偏好团队 |
| — | ❌ 不推荐裸用 | 用 Redux Toolkit | |
| — | ❌ Meta 已停止维护 | 迁移到 Jotai |
⚠️ React 19 之后 Redux 还需要吗?
Server Actions + useActionState + useOptimistic 把"表单 + 加载状态 + 乐观更新"这类之前 Redux 的主战场全部内置化。但 Redux 在以下场景仍不可替代:① 复杂客户端状态机(多步骤向导、协作编辑);② 需要时间旅行调试;③ 团队已有大量 Redux 代码不想迁移。新项目首选 Zustand + TanStack Query 组合。
5. React Router(v6)
单页应用(SPA)通过浏览器 History API(pushState / replaceState)实现路由切换,不真正发起页面请求。
核心 API
import { BrowserRouter, Routes, Route, Link, useNavigate, useParams, Navigate } from 'react-router-dom'
// 应用根部配置路由
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">首页</Link>
<Link to="/users">用户列表</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users" element={<UserList />} />
<Route path="/users/:id" element={<UserDetail />} />
{/* 路由守卫:未登录跳转到 /login */}
<Route path="/admin" element={<PrivateRoute><Admin /></PrivateRoute>} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
)
}
// 动态路由参数
function UserDetail() {
const { id } = useParams<{ id: string }>()
return <div>用户 ID: {id}</div>
}
// 编程式导航
function LoginButton() {
const navigate = useNavigate()
return <button onClick={() => navigate('/dashboard')}>登录</button>
}
// 路由守卫
function PrivateRoute({ children }: { children: React.ReactNode }) {
const isLoggedIn = useAuthStore(s => s.isLoggedIn)
return isLoggedIn ? children : <Navigate to="/login" replace />
}6. 性能优化
React.memo — 避免不必要的重渲染
// 父组件重渲染时,子组件 props 未变则跳过重渲染
const ExpensiveChild = React.memo(function ExpensiveChild({
data,
onClick,
}: {
data: string
onClick: () => void
}) {
console.log('ExpensiveChild 渲染')
return <div onClick={onClick}>{data}</div>
})
function Parent() {
const [count, setCount] = useState(0)
// ❌ 每次 Parent 渲染都会创建新函数,导致 ExpensiveChild 重渲染
// const handleClick = () => console.log('clicked')
// ✅ useCallback 缓存函数引用
const handleClick = useCallback(() => console.log('clicked'), [])
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<ExpensiveChild data="hello" onClick={handleClick} />
</div>
)
}代码分割 — React.lazy + Suspense
import { lazy, Suspense } from 'react'
// 路由级别的代码分割:只在需要时加载对应 chunk
const UserDashboard = lazy(() => import('./pages/UserDashboard'))
const Settings = lazy(() => import('./pages/Settings'))
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/dashboard" element={<UserDashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
)
}虚拟列表 — 大数据量渲染
渲染 10000 条数据时,只渲染可视区域内的节点:
// 使用 react-window 或 react-virtual 库
import { FixedSizeList } from 'react-window'
function BigList({ items }: { items: string[] }) {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>{items[index]}</div>
)
return (
<FixedSizeList height={600} itemCount={items.length} itemSize={40} width="100%">
{Row}
</FixedSizeList>
)
}典型场景与最佳实践
表单处理:受控 vs 非受控组件
// 受控组件:表单数据由 React state 管理(推荐)
function ControlledForm() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
console.log({ name, email })
}
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={e => setName(e.target.value)}
placeholder="姓名"
/>
<input
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="邮箱"
/>
<button type="submit">提交</button>
</form>
)
}
// 非受控组件:通过 ref 直接访问 DOM(适合文件上传等特殊场景)
function UncontrolledForm() {
const nameRef = useRef<HTMLInputElement>(null)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
console.log(nameRef.current?.value)
}
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} defaultValue="" placeholder="姓名" />
<button type="submit">提交</button>
</form>
)
}| 对比项 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据来源 | React state | DOM 自身 |
| 实时验证 | 容易 | 需要手动触发 |
| 典型场景 | 大多数表单场景 | 文件上传、集成第三方库 |
数据请求与 cleanup
// 推荐使用 React Query 或 SWR,避免手写 useEffect 请求
import useSWR from 'swr'
function UserProfile({ id }: { id: number }) {
const { data, error, isLoading } = useSWR(`/api/users/${id}`, fetcher)
if (isLoading) return <div>加载中...</div>
if (error) return <div>请求失败</div>
return <div>{data.name}</div>
}组件通信模式
| 通信方向 | 方式 |
|---|---|
| 父 → 子 | props 传递 |
| 子 → 父 | 父组件传递回调函数(props callback) |
| 兄弟组件 | 状态提升到共同父组件 |
| 跨层级 | Context API |
| 全局复杂状态 | Redux / Zustand 等状态库 |
React Server Components (RSC) 与 Next.js 15
React Server Components(RSC)是 React 18.x / 19 引入、Next.js 13 App Router 开始大规模生产化的核心范式变革。2025-2026 年前端面试**"什么是 RSC、它和 SSR 有什么区别"**是必问题。
Server Component vs Client Component vs SSR
| 维度 | 传统 Client Component | SSR(Server-Side Rendering) | Server Component(RSC) |
|---|---|---|---|
| 运行位置 | 浏览器 | 服务端首次渲染 + 浏览器 hydration | 始终在服务端(每次请求都跑) |
| JS 包含 | ✅ 打包到 bundle | ✅ 打包到 bundle | ❌ 零 JS 发到浏览器 |
| 能用 hooks | ✅ 全部 | ✅ 全部 | ❌(无 useState/useEffect) |
| 能直接读数据库 | ❌(要经 API) | ❌ | ✅ 直接 SQL/ORM 调用 |
| 能用 async/await | ❌(要 hooks) | ❌ | ✅ 函数本身可以 async |
| 能保留交互状态 | ✅ | ✅(hydration 后) | ❌(每次重渲染) |
// Server Component(默认)
async function PostList() {
// 直接在组件里查数据库——无需 API Route
const posts = await db.post.findMany();
return <ul>{posts.map(p => <PostItem key={p.id} post={p} />)}</ul>;
}
// Client Component(需要 'use client' 显式声明)
'use client';
export function LikeButton({ postId }) {
const [liked, setLiked] = useState(false); // ← 能用 hooks
return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>;
}RSC 解决了什么
| 痛点(传统 SSR + CSR) | RSC 怎么解 |
|---|---|
| 客户端 bundle 越来越大(React 应用 200KB+ 起步) | 服务端组件不打包到 bundle |
| 组件查数据要走 API 层(API → DB → API → UI) | 组件直接读数据库,消除中间 API |
| Waterfall 数据请求 | 服务端并行解决 |
| 敏感数据(API Key、用户隐私)泄露风险 | 服务端代码永不发到浏览器 |
| Hydration 慢 + 闪烁 | Streaming + Partial Hydration |
Next.js 15 关键能力(2024-2025)
| 能力 | 解决问题 | 用法 |
|---|---|---|
| App Router | RSC 默认架构 | app/ 目录、文件即路由 |
| Server Actions | 表单提交无需手写 API | 'use server' 标记函数,前端直接调 |
| Partial Pre-rendering(PPR) | 静态壳 + 动态内容流式塞入 | 一个页面既能 CDN 缓存又有实时内容 |
| Turbopack(Rust 写的打包器) | 替代 Webpack,dev 启动快 700× | next dev --turbo |
use cache 指令 | 细粒度缓存控制 | 标记任意函数/组件结果可缓存 |
| Streaming + Suspense | 首屏快、其他部分逐步流式渲染 | <Suspense fallback={...}> |
Server Actions 实战
替代了 99% 的 useState + fetch + try/catch 表单代码:
// app/actions.ts
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await db.post.create({ data: { title } });
revalidatePath('/posts'); // 自动失效缓存
}
// app/page.tsx
import { createPost } from './actions';
export default function NewPostPage() {
return (
<form action={createPost}> {/* ← 直接传 server function */}
<input name="title" />
<button>发布</button>
</form>
);
}💡 面试加分点
能讲清 "Server Action 本质是 Next.js 帮你自动生成了 POST 端点 + 序列化 + 调用 + revalidate" ——这个抽象层级的理解会让面试官觉得你跟得上前沿。
RSC 边界规则(最常踩的坑)
Server Component ─可以引入→ Client Component (会被打包到 bundle)
Client Component ─不能引入→ Server Component (编译报错)
Client Component ─可以接收 RSC 渲染结果作为 children/props错误示例(最常见):
// ❌ 错误:Client Component 不能直接 import Server Component
'use client';
import ServerThing from './server-thing'; // 编译报错
// ✅ 正确:作为 children 传入
'use client';
export function Layout({ children }) {
return <div>{children}</div>; // children 可以是任何东西
}
// 在 Server Component 中组合:<Layout><ServerThing /></Layout>何时不用 RSC
| 场景 | 原因 |
|---|---|
| 纯静态站点 | Vite + 静态 SPA 更简单 |
| 完全离线优先 PWA | Service Worker 主导,服务端组件意义不大 |
| 重度交互应用(如 Figma、在线 IDE) | 几乎所有组件都是 Client,RSC 没有收益 |
| 不想锁 Next.js 生态 | RSC 目前主要落地在 Next.js / Remix v3 |
面试黄金回答
"RSC 是 React 把'查数据 + 渲染 HTML'下沉到服务端的范式,核心收益是零 JS Bundle + 直接访问数据库 + 消除 API 中间层。它和 SSR 的区别是 SSR 还需要 hydration(同一份代码跑两次),RSC 服务端代码永远不发到浏览器。Next.js 15 配合 Server Actions 和 Partial Pre-rendering 让 RSC 落地到生产,是 2025 年 React 项目的默认选择。"
React 19 三大革命性特性(2024.12 GA,2026 必考)
React 19 于 2024.12 正式 GA。面试高频 Top 问题:"React 19 最重要的 3 个变化是什么?"——能讲出这三个立刻区分初/中级。
1. React Compiler(自动 memo)—— 告别 useMemo/useCallback
问题:以前 React 需要手动治理重渲染,到处 useMemo / useCallback / React.memo——代码丑且容易遗漏。
React Compiler(原名 Forget):编译期自动插入 memo 提示,零运行时开销。
// ⚠️ 传统写法:手动优化
function TodoList({ todos }) {
const completed = useMemo(
() => todos.filter(t => t.done).length,
[todos]
);
const handleClick = useCallback(id => toggle(id), []);
return <Item count={completed} onClick={handleClick} />;
}
// ✅ React 19 + Compiler:原始写法即可
function TodoList({ todos }) {
const completed = todos.filter(t => t.done).length;
const handleClick = id => toggle(id);
return <Item count={completed} onClick={handleClick} />;
}
// ↑ Compiler 自动判断依赖、插入 memo。性能同手写或更优。开启方式:
// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', { compilationMode: 'annotation' }],
],
};面试加分:"Compiler 是 Rules of Hooks 的隆重抢滩——只要代码遵守 Rules of React,编译器能静态分析依赖。不遵守会发警告"。
2. Actions / useOptimistic / useFormStatus
问题:表单提交需手写 useState + fetch + try/catch + loading,到处重复。
React 19 的 Actions + 3 个高阶 hook:
// useOptimistic: 乐观更新
import { useOptimistic, useTransition } from 'react';
function LikeButton({ initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [optimisticLikes, addOptimistic] = useOptimistic(likes);
const [isPending, startTransition] = useTransition();
return (
<button onClick={() => {
startTransition(async () => {
addOptimistic(optimisticLikes + 1); // ✅ 立即更新 UI
const result = await like(); // 背后请求
setLikes(result); // 最终同步
});
}}>
❤️ {optimisticLikes}
</button>
);
}
// useFormStatus: 子组件中获取表单状态不需透传
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? '提交中...' : '提交'}</button>;
}
// useActionState: 表单状态 + Action 绑定
function Form() {
const [error, submitAction, pending] = useActionState(
async (prevState, formData) => {
const r = await fetch('/api', { body: formData });
return r.ok ? null : '提交失败';
},
null
);
return <form action={submitAction}>{error && <p>{error}</p>}<SubmitButton /></form>;
}3. ref 作为 prop + 取消 forwardRef + use API
// ⚠️ React 18 与之前:需 forwardRef 包裹
const MyInput = forwardRef((props, ref) => <input {...props} ref={ref} />);
// ✅ React 19:ref 直接是 prop
function MyInput({ ref, ...props }) {
return <input {...props} ref={ref} />;
}
// ✅ use API: 在 Render 中读 Promise 或 Context,无需 hook
import { use } from 'react';
function UserPanel({ userPromise }) {
const user = use(userPromise); // ⚠️ 会触发 Suspense
return <div>{user.name}</div>;
}React 19 其他重要变化速查
| 变化 | 说明 |
|---|---|
原生支持 <title> / <meta> / <link> | 任何组件内都可写,React 自动提升到 <head> |
| 原生支持异步类型资源 | 可以 import 样式表、脚本,React 自动处理 dedup |
| Document Metadata | SEO 场景不再需 react-helmet |
| 更好的错误信息 | 合并重复错误、产包堆栈更清晰 |
| Server Components / Server Actions 正式 GA | 不再是 experimental |
迁移 React 18 → 19 检查清单
① 检查是否还在用 defaultProps——函数组件已不支持,改用默认参数; ② propTypes 已移除,远期迁到 TypeScript; ③ 清理 forwardRef——不是 bug,但代码会变净; ④ 检查是否启 React Compiler——需代码遵守 Rules of React。
微前端与 Module Federation(2026 大型 Web 必考)
💡 何时上微前端
不要为了"用而用"。微前端只在以下场景才回本:① 多团队并行开发同一站点(如阿里、字节内部门户);② 巨型应用拆分(百万行级);③ 技术栈渐进升级(一部分还是 jQuery,一部分要上 React);④ 独立部署 / 独立发版。普通 SaaS / 中后台不需要微前端。
主流方案对比(2026)
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Module Federation(Webpack 5 / Rspack) | 运行时跨应用共享模块 | ✅ 共享依赖(不重复下载 React);✅ 主子框架自由 | 配置复杂;版本协议要约定 |
| qiankun(阿里) | 基于 single-spa + 沙箱 | ✅ 沙箱隔离强;中文生态好 | 性能损耗;CSS/JS 隔离仍有边界 |
| micro-app(京东) | Web Components + JS 沙箱 | ✅ 接入极简(一个标签) | 生态较新 |
| wujie(腾讯) | iframe + WebComponent | ✅ 真隔离(iframe);性能优于传统 iframe | iframe 通信复杂 |
| Native iframe | 浏览器原生 | ✅ 最强隔离 | 通信差、SEO 差、UX 割裂 |
Module Federation 关键概念
// host(主应用)
new ModuleFederationPlugin({
name: 'shell',
remotes: {
// 运行时加载远程模块
cart: 'cart@https://cdn.example.com/cart/remoteEntry.js',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } }
});
// remote(子应用)
new ModuleFederationPlugin({
name: 'cart',
filename: 'remoteEntry.js',
exposes: { './App': './src/App' },
shared: { react: { singleton: true }, 'react-dom': { singleton: true } }
});// host 中按需异步加载
const CartApp = React.lazy(() => import('cart/App'));微前端核心问题与对策
| 问题 | 对策 |
|---|---|
| JS 全局变量污染 | 沙箱(Proxy 拦截 window 写入) |
| CSS 样式冲突 | Shadow DOM / CSS Modules / scoped 前缀 |
| 依赖版本不一致 | Module Federation shared.singleton 强制单例;约定主应用版本 |
| 路由冲突 | 主应用做路由分发;子应用用 hash 路由或 basename |
| 跨应用通信 | 全局事件总线(CustomEvent);状态共享(避免);URL 参数 |
| 共享登录态 | Cookie + 同域;JWT 走 localStorage 或 HTTP-Only Cookie |
面试黄金答法:「微前端不是技术问题,是组织协作问题。一个团队能搞定的项目永远不应该微前端化——增加的协调成本远超收益。判断信号:是否多团队?是否独立发版?是否技术栈不一致?三个都是 yes 才考虑」。
面试常问 & 怎么答
Q1:虚拟 DOM 是什么?为什么需要它?
虚拟 DOM 是用 JS 对象描述 UI 结构的轻量级树。需要它的原因:直接操作真实 DOM 代价高(触发回流重绘),虚拟 DOM 通过 Diff 算法计算最小变更集,批量更新真实 DOM。更重要的是,它提供了声明式编程模型(你描述"应该是什么",React 决定"怎么做到"),以及跨平台能力(同一套组件逻辑可运行在 Web 和 Native 上)。
Q2:React 的 key 有什么作用?为什么不能用 index?
key 是 React 在列表 Diff 时识别节点身份的标识符。当列表项重排或增删时,React 通过 key 判断哪些节点可以复用,哪些需要销毁重建。
使用 index 作为 key 的问题:当列表顺序发生变化(插入、删除、排序),index 对应的元素变了,React 会误以为是"同一个节点内容变了",导致组件状态错乱(比如受控输入框显示错误的值)、不必要的 DOM 操作。
结论:key 应使用数据的唯一稳定标识(如 id),只有列表是静态不变的情况下才可以用 index。
Q3:useEffect 的依赖数组是什么?空数组和不传的区别?
依赖数组告诉 React "当哪些值发生变化时重新执行这个 effect"。
- 不传:每次组件渲染后都执行,通常会导致性能问题或无限循环
[]空数组:只在组件挂载(mount)后执行一次,相当于componentDidMount[dep1, dep2]:挂载后执行,且在dep1或dep2变化时重新执行
cleanup 函数在下一次 effect 执行前或组件卸载时调用,用于清除订阅、计时器等。
Q4:useState 的更新是同步还是异步?
useState 的更新是异步批量的,不是立即同步更新 state 值。调用 setState 后,当前执行上下文中读取的 state 仍是旧值,要在下次渲染中才能读到新值。
React 18 引入 Automatic Batching,即使在 setTimeout、原生事件处理器中的多次 setState 也会被批量合并为一次渲染(React 17 只在事件处理函数中批量)。
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
console.log(count) // 仍然是旧值 0,不是 1
}
// 如果需要基于最新值更新,用函数形式
const handleClickSafe = () => {
setCount(prev => prev + 1) // ✅ 始终基于最新值
}Q5:React 的生命周期(Hooks 对应关系)?
| 类组件生命周期 | Hooks 等价写法 |
|---|---|
componentDidMount | useEffect(() => { ... }, []) |
componentDidUpdate | useEffect(() => { ... }, [dep]) |
componentWillUnmount | useEffect(() => { return () => { cleanup } }, []) |
shouldComponentUpdate | React.memo + useMemo |
getDerivedStateFromProps | 在渲染函数中直接计算(或 useMemo) |
Q6:类组件和函数组件的区别?
核心区别:函数组件是纯函数,每次渲染都是一次独立的函数调用,闭包捕获当时的 props 和 state;类组件通过 this 访问最新值,this.props/this.state 始终指向最新值。
这导致一个经典差异:函数组件中的 event handler 会"记住"创建时的 state 值(闭包),而类组件中的 handler 通过 this.state 总能读到最新值。
Q7:什么是受控组件和非受控组件?
- 受控组件:表单元素的值由 React state 控制,每次用户输入都触发
onChange更新 state,input 的value始终来自 state。优点:可以实时验证、格式化输入。 - 非受控组件:表单元素维护自己的内部状态,通过
ref在需要时读取值(类似传统 DOM 操作)。适合文件上传等场景。
Q8:React 如何做性能优化?
- 减少不必要的重渲染:
React.memo(组件级)、useMemo(值缓存)、useCallback(函数缓存) - 状态合理分层:避免状态放太高,Context 拆分细粒度(只让关心的组件订阅)
- 代码分割:
React.lazy+Suspense按路由/功能懒加载 - 虚拟列表:大数据量列表使用
react-window/react-virtual - 避免内联对象/函数:
<Child style={ { color: 'red' } } />每次渲染都是新对象,配合React.memo会失效 - 生产构建:确保使用 production build(移除 DevTools、断言等)
Q9:useCallback 和 useMemo 的区别?
// useMemo:缓存计算结果(值)
const expensiveValue = useMemo(() => compute(a, b), [a, b])
// useCallback:缓存函数引用(本质是 useMemo 的语法糖)
const handleClick = useCallback(() => doSomething(a), [a])
// 等价于:
const handleClick = useMemo(() => () => doSomething(a), [a])使用时机:
useMemo:计算代价高、或需要稳定引用的对象(配合React.memo的子组件)useCallback:传给子组件的回调函数,且子组件用了React.memo
过度使用
useMemo/useCallback反而有性能负担,先 profile,再优化。
Q10:Context 的缺点是什么?什么时候用 Redux?
Context 的缺点:
- 性能问题:Context value 变化时,所有消费该 Context 的组件都会重渲染(即使只用了其中一部分数据)
- 调试困难:不像 Redux 有 DevTools 支持时间旅行调试
- 不适合高频更新:如鼠标位置、实时计数等,会导致大量组件频繁重渲染
使用 Redux 的时机:
- 全局状态量大、更新频繁
- 多个组件需要响应同一状态变化
- 需要中间件处理异步逻辑(redux-thunk / redux-saga)
- 需要时间旅行调试、状态持久化等高级功能
现代项目更多选择 Zustand(轻量)或 Redux Toolkit(大型项目),直接用原始 Redux 的场景已较少。
常见陷阱
| 陷阱 | 问题描述 | 解决方案 |
|---|---|---|
| useEffect 闭包陷阱 | effect 中读取的 state/props 是创建时的旧值,而非最新值 | 将变量加入依赖数组;或使用 useRef 存储最新值 |
| 直接修改 state | state.list.push(item) 不会触发重渲染,因为引用未变 | 始终返回新对象/数组:setState([...prev, item]) |
| key 使用 index | 列表重排时组件状态错乱(如 input 值对应错误) | 使用数据的唯一稳定 id 作为 key |
| useEffect 无限循环 | 依赖项中包含每次渲染都会重建的对象/函数 | 用 useMemo/useCallback 稳定引用;或重新检查依赖项 |
| 在条件语句中使用 Hook | 违反 Hooks 规则,React 无法保证 Hook 调用顺序一致 | 始终在函数顶层调用 Hook,条件逻辑放 Hook 内部 |
| 忘记 cleanup | 组件卸载后仍然 setState,导致内存泄漏和控制台警告 | 在 useEffect 返回 cleanup 函数取消订阅/请求 |
| 不必要的重渲染 | 父组件 props 未变但子组件仍重渲染 | React.memo 包裹子组件 + useCallback 稳定回调引用 |
看到什么就先想到这类
| 关键词/场景 | 联想到 |
|---|---|
| "UI 不更新" | 是否直接修改了 state(没返回新对象);key 是否有问题 |
| "重复渲染/性能" | React.memo + useCallback + useMemo;是否有不稳定引用 |
| "获取最新 state" | useEffect 闭包陷阱 → 检查依赖数组;或用 useRef 存最新值 |
| "组件间共享状态" | 状态提升 → Context → 状态管理库,根据更新频率和复杂度选择 |
| "大列表卡顿" | 虚拟列表(react-window / react-virtual) |
| "首屏加载慢" | 代码分割 React.lazy + Suspense;路由懒加载 |
| "表单" | 受控组件 useState 管理;复杂表单用 react-hook-form |
| "数据请求" | 避免裸 useEffect,优先用 SWR / React Query |
| "路由跳转" | useNavigate;权限控制用 PrivateRoute 包裹 |
| "跨层传值" | useContext;避免 props drilling |