React 高频面试题
1. React 的核心概念是什么?
React 是一个用于构建用户界面的 JavaScript 库 ,核心思想:UI = f(state)
| 核心概念 | 说明 |
|---|---|
| JSX | JavaScript 语法扩展,编译为 React.createElement() 调用 |
| 组件 | UI 的基本构建单元,分函数组件和类组件 |
| 单向数据流 | 数据从父组件通过 props 向下流动 |
| 虚拟 DOM | 内存中的轻量 DOM 表示,通过 diff 算法最小化真实 DOM 操作 |
| 声明式 | 描述 UI 应该是什么样子,而不是如何操作 DOM |
jsx
// ========== 函数组件(推荐) ==========
const Greeting = ({ name, age = 18 }) => {
return (
<div className="greeting">
<h1>Hello, {name}!</h1>
<p>Age: {age}</p>
</div>
)
}
// ========== JSX 本质 ==========
// JSX 是 React.createElement 的语法糖
// <div className="app">Hello</div>
// 编译为:
React.createElement('div', { className: 'app' }, 'Hello')
// JSX 中的条件渲染
const App = ({ isLoggedIn, items }) => (
<div>
{/* 三元表达式 */}
{isLoggedIn ? <Dashboard /> : <Login />}
{/* 短路求值 */}
{isLoggedIn && <Welcome />}
{/* 列表渲染(必须有 key) */}
{items.map(item => <Item key={item.id} data={item} />)}
</div>
)
// ========== 函数组件 vs 类组件 ==========
// ✅ 函数组件(现代 React 推荐)
function FuncComp({ count }) {
// 每次渲染都捕获当前的 props/state(闭包特性)
const handleClick = () => {
setTimeout(() => alert(count), 3000) // 总是弹出点击时的 count
}
return <button onClick={handleClick}>{count}</button>
}
// ❌ 类组件(this.props 是可变的)
class ClassComp extends React.Component {
handleClick = () => {
setTimeout(() => alert(this.props.count), 3000) // 弹出最新的 count(可能已变)
}
render() {
return <button onClick={this.handleClick}>{this.props.count}</button>
}
}
💡 面试加分点: 函数组件和类组件的本质区别不仅是语法------函数组件每次渲染都捕获当前的 props 和 state (闭包),而类组件通过
this读取的总是最新值。这就是著名的 "Capture Value" 特性。React 官方已明确推荐函数组件 + Hooks。
2. React 的 Context 是什么?
Context 提供了一种在组件树中共享数据的方式,无需通过 props 逐层传递。
| 对比项 | Props Drilling | Context | 状态管理库 |
|---|---|---|---|
| 层级限制 | 逐层传递 | 跨层级 | 跨层级 |
| 性能 | ✅ 按需渲染 | ⚠️ Provider value 变化时所有消费者重渲染 | ✅ 精确更新 |
| 适用场景 | 1-2 层 | 主题/语言/认证等低频变化 | 高频更新的全局状态 |
jsx
import { createContext, useContext, useState, useMemo } from 'react'
// ========== 创建 Context ==========
const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} })
// ========== Provider 组件 ==========
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light')
const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light')
// ⚠️ 必须用 useMemo 缓存 value,否则 Provider 每次渲染都创建新对象
// 导致所有消费者都重渲染
const value = useMemo(() => ({ theme, toggleTheme }), [theme])
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
)
}
// ========== 自定义 Hook 封装(推荐模式) ==========
const useTheme = () => {
const context = useContext(ThemeContext)
if (!context) throw new Error('useTheme must be used within ThemeProvider')
return context
}
// ========== 消费 Context ==========
const ThemedButton = () => {
const { theme, toggleTheme } = useTheme()
return (
<button
style={{ background: theme === 'dark' ? '#333' : '#fff', color: theme === 'dark' ? '#fff' : '#333' }}
onClick={toggleTheme}
>
切换主题(当前:{theme})
</button>
)
}
// ========== 多 Context 组合 ==========
const AuthContext = createContext(null)
const I18nContext = createContext('zh')
const App = () => (
<AuthProvider>
<ThemeProvider>
<I18nProvider>
<Page />
</I18nProvider>
</ThemeProvider>
</AuthProvider>
)
// ========== 性能优化:拆分 Context ==========
// ❌ 错误:一个大 Context 放所有状态
const AppContext = createContext({ theme: '', user: null, locale: '' })
// ✅ 正确:按职责拆分为多个小 Context
const ThemeCtx = createContext('light')
const UserCtx = createContext(null)
const LocaleCtx = createContext('zh')
// 修改 theme 不会导致只用 user 的组件重渲染
💡 面试加分点:
Context最大的性能问题是 Provider value 变化时,所有消费者都会重渲染,即使它们只用了 value 中的部分属性。解决方案:
1️⃣
useMemo缓存 value2️⃣ 拆分为多个小 Context
3️⃣ 使用
React.memo阻断传播4️⃣ 如果状态更新频繁,考虑用 Zustand/Jotai 替代。
3. React 中 key 的作用和 Reconciliation 原理
key 的核心作用
key 是 React 在 Reconciliation(协调/调和)过程中用来标识元素身份的唯一属性,决定了元素是"复用更新"还是"销毁重建"。
| 场景 | 有唯一 key | 无 key(或用 index) |
|---|---|---|
| 列表增删 | ✅ 精确匹配,最小化 DOM 操作 | ❌ 按位置复用,可能状态错乱 |
| 列表排序 | ✅ 移动 DOM 节点 | ❌ 逐个更新内容,效率低 |
| 条件切换 | ✅ key 不同 → 重建组件 | key 相同 → 复用(可能有 Bug) |
为什么不能用 index 做 key?
jsx
// ========== index 做 key 的经典 Bug ==========
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React' },
{ id: 2, text: 'Learn Vue' },
{ id: 3, text: 'Learn Angular' }
])
const removeFirst = () => {
setTodos(todos.slice(1)) // 删除第一项
}
return (
<div>
{/* ❌ 用 index 做 key */}
{todos.map((todo, index) => (
<div key={index}>
<span>{todo.text}</span>
<input defaultValue={todo.text} />
{/* 删除 "Learn React" 后:
index=0 对应 "Learn Vue",但 input 还是 "Learn React" 的内容!
因为 React 认为 key=0 的元素还在,只是更新了 text prop
而 input 是非受控的,defaultValue 不会再更新 → 状态错乱! */}
</div>
))}
{/* ✅ 用唯一 id 做 key */}
{todos.map(todo => (
<div key={todo.id}>
<span>{todo.text}</span>
<input defaultValue={todo.text} />
{/* 删除 "Learn React" 后:key=1 的 DOM 被移除,key=2 和 key=3 复用 → 正确! */}
</div>
))}
<button onClick={removeFirst}>删除第一项</button>
</div>
)
}
key 在 Reconciliation 中的工作原理
jsx
// ========== React Diff 中 key 的匹配逻辑 ==========
// React 的 Reconciliation 规则:
// 1. 不同类型的元素 → 销毁旧树,创建新树
// 2. 相同类型的 DOM 元素 → 只更新变化的属性
// 3. 相同类型的组件 → 保留实例,更新 props
// 4. 列表中的元素 → 用 key 匹配新旧节点
// 有 key 的列表 diff 过程(简化):
// 旧: [A:key1] [B:key2] [C:key3]
// 新: [C:key3] [A:key1] [B:key2]
//
// 第一轮遍历:逐位比较(A vs C → key 不同,跳出)
// 构建旧节点 Map:{ key1: A, key2: B, key3: C }
// 遍历新列表:
// C:key3 → Map 中找到,复用并移动
// A:key1 → Map 中找到,复用并移动
// B:key2 → Map 中找到,复用并移动
// 结果:只是移动了 DOM 节点,没有销毁重建
// ========== key 的高级用法:强制重建组件 ==========
function App() {
const [userId, setUserId] = useState(1)
return (
<div>
{/* ✅ key 变化 → UserProfile 整个重建(重置所有状态) */}
<UserProfile key={userId} userId={userId} />
{/* 不用 key 或 key 不变 → 组件复用,只更新 props */}
{/* 如果组件内部有 useState 等状态,状态不会重置 */}
<button onClick={() => setUserId(2)}>切换用户</button>
</div>
)
}
// ========== key 重建 vs useEffect 重置 ==========
// 方式1:key 变化强制重建(简单粗暴,推荐)
<Form key={formId} initialData={data} />
// 方式2:useEffect 监听 prop 变化重置状态(需要手动维护)
function Form({ initialData }) {
const [formData, setFormData] = useState(initialData)
useEffect(() => {
setFormData(initialData) // 需要手动重置每个状态
}, [initialData])
}
key 的本质:JSX 元素的"身份证"
jsx
// ========== key 是 React Element 的特殊属性 ==========
// JSX 编译后:
<li key="1">A</li>
// ↓ 编译为
React.createElement('li', { key: '1' }, 'A')
// ↓ 生成 ReactElement
{ type: 'li', key: '1', props: { children: 'A' }, ref: null, $$typeof: Symbol(react.element) }
// 注意:key 不是普通 props,组件内部无法通过 props.key 访问
function Item(props) {
console.log(props.key) // ❌ undefined(key 被 React 拦截)
console.log(props.id) // ✅ 正常访问
return <li>{props.id}</li>
}
// 如果需要在组件内拿到 key,必须额外传:<Item key={id} id={id} />
key 的取值规则与最佳实践
| 取值方式 | 是否推荐 | 说明 |
|---|---|---|
| 业务唯一 ID(数据库主键) | ✅ 强烈推荐 | 最稳定,数据生命周期内不变 |
| UUID(创建时生成并保存) | ✅ 推荐 | 适合前端纯本地数据(如待办事项) |
多字段拼接(如 ${type}-${id}) |
✅ 推荐 | 跨数据源合并列表时使用 |
| 数组 index | ⚠️ 仅在静态列表使用 | 列表只渲染不增删不排序才安全 |
Math.random() / Date.now() |
❌ 禁止 | 每次渲染都变 → 完全失去 diff 优化,列表整体重建 |
对象引用(如 key={item}) |
❌ 禁止 | React 会调用 String(key) → [object Object],全部冲突 |
| 不传 key | ⚠️ 默认用 index | React 会警告(开发环境),等价于 index |
jsx
// ========== 不同场景下的 key 选择 ==========
// 场景1:后端返回的数据 → 用业务 id
{users.map(u => <UserItem key={u.id} user={u} />)}
// 场景2:纯前端创建的临时数据 → 创建时生成稳定 id
const addTodo = (text) => {
setTodos([...todos, { id: crypto.randomUUID(), text }]) // ✅ 创建时生成
// ❌ 不要在 render 里 todos.map((t, i) => ({ ...t, id: crypto.randomUUID() }))
}
// 场景3:合并多个数据源 → 加前缀
{[
...localPosts.map(p => ({ ...p, _key: `local-${p.id}` })),
...remotePosts.map(p => ({ ...p, _key: `remote-${p.id}` }))
].map(p => <Post key={p._key} {...p} />)}
// 场景4:嵌套数组 / Fragment
{groups.map(group => (
<React.Fragment key={group.id}> {/* Fragment 也支持 key */}
<h3>{group.name}</h3>
{group.items.map(item => (
<Item key={item.id} data={item} /> {/* 内层独立 key */}
))}
</React.Fragment>
))}
// 注意:<>...</> 短语法不支持 key,必须用 React.Fragment
key 的作用域:兄弟节点之间唯一即可
jsx
// ========== key 只在同一父节点的兄弟节点间比较 ==========
<div>
<ul>
<li key="1">A</li> {/* 这个 1 和下面的 1 不冲突 */}
<li key="2">B</li>
</ul>
<ul>
<li key="1">X</li> {/* 不同的父节点,key 可以重复 */}
<li key="2">Y</li>
</ul>
</div>
// ❌ 但同一父节点下重复 key 会警告并导致 diff 错乱
<ul>
<li key="1">A</li>
<li key="1">B</li> {/* Warning: Encountered two children with the same key */}
</ul>
key 强制重建组件的进阶场景
jsx
// ========== 场景1:路由切换重置表单 ==========
function EditPage() {
const { userId } = useParams()
// 切换 userId 时,整个 UserForm 重建,所有 useState/useRef 都重置
return <UserForm key={userId} userId={userId} />
}
// ========== 场景2:错误边界重置 ==========
function App() {
const [errorKey, setErrorKey] = useState(0)
return (
<ErrorBoundary key={errorKey} onReset={() => setErrorKey(k => k + 1)}>
<Dashboard />
</ErrorBoundary>
)
}
// ========== 场景3:动画重新触发 ==========
function Toast({ message }) {
// 每次 message 变化,整个组件重建,CSS 动画重新执行
return <div key={message} className="toast-fade-in">{message}</div>
}
// ========== 场景4:受控 → 非受控切换重置 ==========
// React 官方推荐:用 key 重置非受控组件状态
<input key={resetCount} defaultValue="" />
// resetCount++ → input 重建 → 清空内容
key 相关的常见坑
jsx
// ========== 坑1:用 index 做 key + 有状态子组件 ==========
{list.map((item, i) => <ItemWithState key={i} data={item} />)}
// 删除中间一项时:后面所有 ItemWithState 内部状态都会"前移"错位
// ========== 坑2:动态生成的 key 每次都不同 ==========
function Bad() {
return list.map(item => <Item key={`${item.id}-${Date.now()}`} />)
// 每次渲染 Date.now() 都变 → 整个列表全部销毁重建
}
// ========== 坑3:key 类型不一致 ==========
<li key={1}>A</li> // key 实际是字符串 "1"
<li key="1">B</li> // 也是 "1"
// React 内部会 toString,数字 1 和字符串 "1" 视为相同 key
// ========== 坑4:动画/过渡场景错把 key 当业务标识 ==========
// react-transition-group / framer-motion 的 AnimatePresence
// 必须保证 key 在元素生命周期内稳定,否则进退场动画异常
<AnimatePresence>
{visible && <motion.div key="modal" exit={{ opacity: 0 }}>...</motion.div>}
</AnimatePresence>
// ========== 坑5:跨父节点移动 ==========
// React 的 diff 不会跨父节点复用 DOM
// 把一个有 key 的节点从 <ul>A 移到 <ul>B → 仍然是销毁+重建
// 想跨容器复用 → 用 createPortal 或保持父节点不变
key 的源码视角(Fiber Reconciliation)
js
// ========== React 源码:reconcileChildrenArray 简化逻辑 ==========
// 文件:react-reconciler/src/ReactChildFiber.js
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
// 阶段1:从头开始按顺序匹配(fast path)
let oldFiber = currentFirstChild
let newIdx = 0
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// key 相同且 type 相同 → 复用 Fiber
if (oldFiber.key === newChildren[newIdx].key) {
// 更新 props,标记 Placement / Update
} else {
break // key 不匹配 → 跳出,进入阶段3
}
oldFiber = oldFiber.sibling
}
// 阶段2:新列表遍历完,删除剩余旧 Fiber
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber)
return
}
// 阶段3:旧 Fiber 放进 Map(key → Fiber),新列表用 key 查找
const existingChildren = mapRemainingChildren(returnFiber, oldFiber)
for (; newIdx < newChildren.length; newIdx++) {
const matchedFiber = existingChildren.get(
newChildren[newIdx].key ?? newIdx // 没 key 用 index 兜底
)
if (matchedFiber) {
// 复用:从 Map 删除,标记移动
existingChildren.delete(newChildren[newIdx].key ?? newIdx)
} else {
// 没匹配到:创建新 Fiber
}
}
// 阶段4:Map 里剩下的旧 Fiber → 全部删除
existingChildren.forEach(child => deleteChild(returnFiber, child))
}
核心结论 :React 的 diff 是 O(n) 算法,前提是有稳定的 key。没有 key 时退化为按位置比较,列表头部插入会导致全列表重渲染。
面试速答模板
key 是什么:React 给同层兄弟节点的"身份证",是 Reconciliation(协调)阶段匹配新旧 Fiber 的依据。
作用 :让 React 在 O(n) 时间内识别哪些节点是复用、移动、新增、删除,决定 DOM 操作和组件状态去留。
三大规则:
- 稳定------同一数据生命周期内 key 不变(用业务 id,禁用 Math.random)
- 唯一------同层兄弟节点间唯一(不需要全局唯一)
- 可预测------不要用 index 做有状态列表的 key(增删会导致状态错乱)
进阶用法 :用 key 变化强制重建组件,是重置组件状态最优雅的方式(替代 useEffect 监听 prop 重置 state)。
💡 面试加分点: key 的本质是给 Reconciliation 提供"节点身份标识"。三个核心规则:1)列表中用唯一业务 id 做 key ,不要用 index(有状态子元素会错乱);2)用 key 强制重建组件 是重置组件状态的最简单方法(比 useEffect 监听重置更优雅);3)key 必须在兄弟节点中唯一 (不需要全局唯一)。React 的 Fiber 架构中,key 是createFiberFromElement时用于匹配旧 Fiber 的关键依据,源码中通过existingChildrenMap 实现 O(1) 查找,没 key 时退化为 index 匹配,列表头部插入会触发全列表重渲染。
4. React 的 Diff 算法和 Fiber 架构?
Diff 算法(Reconciliation)
React 的 diff 算法基于三个策略将 O(n³) 降为 O(n):
| 策略 | 说明 |
|---|---|
| Tree Diff | 只比较同层级节点,不跨层移动 |
| Component Diff | 不同类型的组件直接替换整棵子树 |
| Element Diff | 同层子元素通过 key 标识移动、新增、删除 |
jsx
// ========== key 的重要性 ==========
// ❌ 用 index 作为 key(列表增删时 DOM 复用错误)
items.map((item, index) => <li key={index}>{item.name}</li>)
// 在头部插入新元素时:所有 li 都会重新渲染(key 全部错位)
// ✅ 用唯一 ID 作为 key
items.map(item => <li key={item.id}>{item.name}</li>)
// 只渲染新插入的 li,其他 li 直接复用
// ❌ 用随机数作为 key
items.map(item => <li key={Math.random()}>{item.name}</li>)
// 每次渲染都是新 key,完全失去 diff 优化
Fiber 架构(React 16+)
| 对比项 | Stack Reconciler(旧) | Fiber Reconciler(新) |
|---|---|---|
| 执行方式 | 递归,同步不可中断 | 链表,异步可中断 |
| 更新优先级 | 无 | 有(紧急/非紧急) |
| 帧率影响 | 长任务阻塞页面 | 时间切片,保证 60fps |
| 并发支持 | ❌ | ✅ |
php
Fiber 工作流程(双缓冲树):
┌─────────────┐ ┌─────────────┐
│ current 树 │ ←→ │ workInProgress│
│ (当前屏幕) │ │ (构建中) │
└─────────────┘ └─────────────┘
↑ │
└── commit 阶段交换 ──┘
Fiber 节点结构(链表):
FiberNode {
type, // 组件类型
key,
stateNode, // 真实 DOM / 类组件实例
child, // 第一个子节点
sibling, // 下一个兄弟节点
return, // 父节点
memoizedState, // 当前 state(Hook 链表)
flags, // 副作用标记(Placement/Update/Deletion)
}
两个阶段:
┌─────────────────────────────┐
│ Render 阶段(可中断) │
│ - 构建 workInProgress 树 │
│ - 标记需要更新的节点(flags) │
│ - 可被高优先级任务打断 │
└─────────────────────────────┘
↓
┌─────────────────────────────┐
│ Commit 阶段(不可中断) │
│ - 操作真实 DOM │
│ - 执行副作用(useEffect 等) │
│ - 一次性同步完成 │
└─────────────────────────────┘
💡 面试加分点: Fiber 的核心是将递归变为链表遍历 ,每个 Fiber 节点是一个工作单元,可以暂停/恢复。React 18 的
useTransition、useDeferredValue依赖 Fiber 的优先级调度。Render 阶段可中断,Commit 阶段不可中断------因为操作真实 DOM 必须一次性完成。
5. React 的性能优化方法?
| 分类 | 优化手段 | 说明 |
|---|---|---|
| 渲染层 | React.memo | 浅比较 props,跳过不必要渲染 |
| useMemo / useCallback | 缓存值和函数引用 | |
| 状态下沉 | 将频繁变化的状态移到更小的子组件 | |
| 避免 Context 滥用 | 拆分 Context,减少不必要的消费者更新 | |
| 加载层 | 代码分割 | React.lazy() + Suspense |
| 路由懒加载 | 按路由动态 import | |
| 图片懒加载 | loading="lazy" / IntersectionObserver |
|
| 列表层 | 虚拟列表 | react-window / react-virtuoso |
| 稳定 key | 不用 index 做 key | |
| 网络层 | 数据缓存 | TanStack Query / SWR |
| 预加载 | <link rel="preload"> / prefetch |
jsx
// ========== 1. React.memo ==========
const Child = React.memo(({ name, onClick }) => {
console.log('Child 渲染')
return <button onClick={onClick}>{name}</button>
})
// ========== 2. 代码分割 + 懒加载 ==========
import { lazy, Suspense } from 'react'
const HeavyPage = lazy(() => import('./HeavyPage'))
// 带 loading 的懒加载
const App = () => (
<Suspense fallback={<div>Loading...</div>}>
<HeavyPage />
</Suspense>
)
// ========== 3. 虚拟列表(react-window) ==========
import { FixedSizeList } from 'react-window'
const VirtualList = ({ items }) => (
<FixedSizeList height={400} itemCount={items.length} itemSize={50} width="100%">
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
)
// ========== 4. 状态下沉(提升性能最有效的方式之一) ==========
// ❌ 整个 App 因为 input 变化而重渲染
const App = () => {
const [text, setText] = useState('')
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<ExpensiveTree /> {/* 每次输入都重渲染 */}
</div>
)
}
// ✅ 将频繁变化的状态下沉到独立组件
const SearchInput = () => {
const [text, setText] = useState('')
return <input value={text} onChange={e => setText(e.target.value)} />
}
const App = () => (
<div>
<SearchInput />
<ExpensiveTree /> {/* 不再重渲染 */}
</div>
)
// ========== 5. 避免在 render 中创建新对象/函数 ==========
// ❌ 每次渲染都创建新对象/函数
<Child style={{ color: 'red' }} onClick={() => doSomething()} />
// ✅ 提取到外部或用 useMemo/useCallback
const style = useMemo(() => ({ color: 'red' }), [])
const handleClick = useCallback(() => doSomething(), [])
<Child style={style} onClick={handleClick} />
💡 面试加分点: 性能优化的优先级:状态下沉 > 组件拆分 > React.memo > useMemo/useCallback 。很多时候不需要
memo,只需要把频繁变化的状态移到更小的子组件就能解决问题。React DevTools 的 Profiler 可以帮助识别不必要的重渲染。
6. 自定义 Hook 是什么?如何编写?
自定义 Hook 是以 use 开头的函数,用于封装可复用的有状态逻辑。
| Hook 规则 | 说明 |
|---|---|
必须以 use 开头 |
React 通过命名约定识别 Hook |
| 只能在函数组件或其他 Hook 中调用 | 不能在普通函数/循环/条件中调用 |
| 调用顺序必须一致 | 不能放在 if/for 里(Hook 依赖调用顺序形成链表) |
jsx
import { useState, useEffect, useCallback, useSyncExternalStore } from 'react'
// ========== 1. useFetch:封装数据请求 ==========
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const refetch = useCallback(() => {
let cancelled = false
setLoading(true)
setError(null)
fetch(url)
.then(res => { if (!res.ok) throw new Error(res.statusText); return res.json() })
.then(data => { if (!cancelled) setData(data) })
.catch(err => { if (!cancelled) setError(err) })
.finally(() => { if (!cancelled) setLoading(false) })
return () => { cancelled = true }
}, [url])
useEffect(() => {
const cleanup = refetch()
return cleanup
}, [refetch])
return { data, loading, error, refetch }
}
// ========== 2. useLocalStorage:本地存储同步 ==========
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setStoredValue = useCallback((newValue) => {
const val = typeof newValue === 'function' ? newValue(value) : newValue
setValue(val)
localStorage.setItem(key, JSON.stringify(val))
}, [key, value])
return [value, setStoredValue]
}
// ========== 3. useDebounce:防抖 ==========
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
// ========== 4. useToggle:开关状态 ==========
function useToggle(initial = false) {
const [value, setValue] = useState(initial)
const toggle = useCallback(() => setValue(v => !v), [])
const setTrue = useCallback(() => setValue(true), [])
const setFalse = useCallback(() => setValue(false), [])
return [value, { toggle, setTrue, setFalse }]
}
// ========== 5. usePrevious:获取上一次的值 ==========
function usePrevious(value) {
const ref = useRef()
useEffect(() => { ref.current = value })
return ref.current
}
// ========== 6. useMediaQuery:响应式断点 ==========
function useMediaQuery(query) {
const subscribe = useCallback(
(callback) => {
const mql = window.matchMedia(query)
mql.addEventListener('change', callback)
return () => mql.removeEventListener('change', callback)
},
[query]
)
const getSnapshot = () => window.matchMedia(query).matches
return useSyncExternalStore(subscribe, getSnapshot)
}
// ========== 使用示例 ==========
const SearchPage = () => {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
const { data, loading, error, refetch } = useFetch(`/api/search?q=${debouncedQuery}`)
const [theme, setTheme] = useLocalStorage('theme', 'light')
const [isOpen, { toggle }] = useToggle(false)
const isMobile = useMediaQuery('(max-width: 768px)')
return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
{loading ? <div>Loading...</div> : <ResultList data={data} />}
{error && <button onClick={refetch}>重试</button>}
</div>
)
}
💡 面试加分点: 自定义 Hook 的本质是逻辑复用 ------将有状态的逻辑从组件中提取出来,多个组件共享同一套逻辑但各自拥有独立的状态 。Hook 规则中「调用顺序必须一致」的原因是 React 内部用链表存储 Hook 状态,靠调用顺序来匹配对应的状态节点。
7. React 18 的新特性?
| 特性 | 说明 |
|---|---|
| createRoot | 新的根 API,启用并发特性 |
| 自动批处理 | 所有场景下的状态更新都自动批处理 |
| useTransition | 标记非紧急更新,可被中断 |
| useDeferredValue | 延迟更新的值,保持 UI 响应 |
| useId | 生成唯一 ID(SSR 安全) |
| Suspense 增强 | 支持 SSR 流式渲染 |
| 严格模式增强 | 开发环境双重调用 Effect |
jsx
// ========== 1. 新根 API ==========
import { createRoot } from 'react-dom/client'
const root = createRoot(document.getElementById('root'))
root.render(<App />)
// 旧 API(React 17):ReactDOM.render(<App />, container)
// ========== 2. 自动批处理(Automatic Batching) ==========
// React 17:只有事件处理函数中的更新会批处理
// React 18:所有场景都自动批处理(setTimeout、Promise、原生事件等)
setTimeout(() => {
setCount(c => c + 1)
setFlag(f => !f)
// React 18:只触发一次重渲染 ✅
// React 17:触发两次重渲染 ❌
}, 0)
// 需要强制不批处理时(极少使用)
import { flushSync } from 'react-dom'
flushSync(() => setCount(c => c + 1)) // 立即更新
flushSync(() => setFlag(f => !f)) // 立即更新
// ========== 3. useTransition:非紧急更新 ==========
import { useState, useTransition } from 'react'
const SearchPage = () => {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
const handleChange = (e) => {
setQuery(e.target.value) // 紧急更新:立即响应输入
startTransition(() => {
setResults(searchData(e.target.value)) // 非紧急:可被中断
})
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending ? <div>搜索中...</div> : <ResultList results={results} />}
</div>
)
}
// ========== 4. useDeferredValue:延迟更新 ==========
import { useDeferredValue, useMemo } from 'react'
const SearchResults = ({ query }) => {
const deferredQuery = useDeferredValue(query)
const isStale = query !== deferredQuery // 是否正在过渡
const results = useMemo(() => searchData(deferredQuery), [deferredQuery])
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<ResultList results={results} />
</div>
)
}
// ========== 5. useId:生成唯一 ID ==========
import { useId } from 'react'
const FormField = ({ label }) => {
const id = useId() // 生成稳定的唯一 ID(如 ":r1:")
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} />
</div>
)
}
// 好处:在 SSR 中客户端和服务端生成相同的 ID,避免 hydration 不匹配
💡 面试加分点:
useTransition和useDeferredValue的区别:useTransition包裹 状态更新的代码 ,而useDeferredValue包裹 值本身 。当你能控制状态更新用useTransition,当值从 props 传入用useDeferredValue。自动批处理是 React 18 最重要的默认行为变化。
8. React 中的错误边界(Error Boundary)?
错误边界是一种 React 组件,能够捕获子组件树中的 JavaScript 错误,显示降级 UI。
| 能捕获 | 不能捕获 |
|---|---|
| 子组件渲染期间的错误 | 事件处理函数中的错误 |
| 生命周期方法中的错误 | 异步代码(setTimeout、Promise) |
| 构造函数中的错误 | 服务端渲染的错误 |
| 错误边界自身的错误 |
jsx
import { Component } from 'react'
// ========== 错误边界(只能用类组件实现) ==========
class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null, errorInfo: null }
}
// 渲染阶段调用:用于返回降级 state
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
// commit 阶段调用:用于记录错误信息
componentDidCatch(error, errorInfo) {
console.error('Error caught:', error, errorInfo)
// 上报到 Sentry 等监控系统
reportError({ error, errorInfo, componentStack: errorInfo.componentStack })
}
handleReset = () => {
this.setState({ hasError: false, error: null, errorInfo: null })
}
render() {
if (this.state.hasError) {
// 自定义 fallback 或默认 UI
return this.props.fallback?.(this.state.error, this.handleReset) || (
<div>
<h2>出错了</h2>
<p>{this.state.error?.message}</p>
<button onClick={this.handleReset}>重试</button>
</div>
)
}
return this.props.children
}
}
// ========== 使用方式 ==========
const App = () => (
<ErrorBoundary fallback={(error, reset) => (
<div>
<p>页面加载失败:{error.message}</p>
<button onClick={reset}>重试</button>
</div>
)}>
<MyComponent />
</ErrorBoundary>
)
// ========== 推荐:使用 react-error-boundary 库 ==========
import { ErrorBoundary } from 'react-error-boundary'
const App = () => (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>重试</button>
</div>
)}
onReset={() => queryClient.invalidateQueries()} // 重试时清除缓存
onError={(error, info) => logErrorToService(error, info)}
>
<Routes />
</ErrorBoundary>
)
// ========== 事件处理中的错误需要自行 try-catch ==========
const Button = () => {
const handleClick = () => {
try {
doSomething()
} catch (error) {
// 错误边界捕获不到这里的错误
reportError(error)
}
}
return <button onClick={handleClick}>Click</button>
}
💡 面试加分点: 错误边界不能用函数组件实现------因为
getDerivedStateFromError和componentDidCatch没有对应的 Hook。生产中推荐使用react-error-boundary库,它提供了useErrorBoundaryHook 让函数组件也能主动触发错误边界。
9. React 中的 Portals 是什么?
Portal 将子组件渲染到 DOM 层级之外的位置,但 React 事件冒泡仍沿组件树传播。
jsx
import { createPortal } from 'react-dom'
// ========== 典型用途:弹窗/Modal ==========
const Modal = ({ isOpen, onClose, children }) => {
if (!isOpen) return null
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{children}
<button onClick={onClose}>关闭</button>
</div>
</div>,
document.body // 渲染到 body 下,避免 z-index 和 overflow 问题
)
}
// ========== 事件冒泡行为 ==========
// 即使 Portal 渲染到了 document.body,事件仍然沿 React 组件树冒泡
const App = () => {
// ✅ 这里能捕获到 Portal 内部按钮的点击事件!
const handleClick = () => console.log('App 捕获到点击')
return (
<div onClick={handleClick}>
<Modal isOpen={true} onClose={() => {}}>
<button>点我</button> {/* 事件会冒泡到 App 的 onClick */}
</Modal>
</div>
)
}
💡 面试加分点: Portal 的关键特性:DOM 层级和 React 组件层级是分离的 ------DOM 上渲染到 body,但 React 事件仍沿组件树冒泡。常用于 Modal、Tooltip、Toast 等需要脱离父容器
overflow: hidden或z-index限制的场景。
10. React 中的 forwardRef 和 useImperativeHandle?
forwardRef 让父组件能获取子组件的 DOM 引用,useImperativeHandle 控制暴露的方法。
jsx
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'
// ========== forwardRef:转发 ref 到子组件 DOM ==========
const MyInput = forwardRef((props, ref) => {
return (
<div className="input-wrapper">
<input ref={ref} {...props} />
</div>
)
})
// ========== useImperativeHandle:控制暴露的方法(而非整个 DOM) ==========
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef(null)
const [value, setValue] = useState('')
// 只暴露特定的方法,而不是整个 DOM 节点
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => { setValue(''); inputRef.current.value = '' },
getValue: () => value,
scrollIntoView: () => inputRef.current?.scrollIntoView({ behavior: 'smooth' }),
}), [value]) // 依赖数组
return <input ref={inputRef} value={value} onChange={e => setValue(e.target.value)} {...props} />
})
// 使用
const Form = () => {
const inputRef = useRef(null)
return (
<div>
<FancyInput ref={inputRef} placeholder="请输入..." />
<button onClick={() => inputRef.current?.focus()}>聚焦</button>
<button onClick={() => inputRef.current?.clear()}>清空</button>
<button onClick={() => alert(inputRef.current?.getValue())}>获取值</button>
</div>
)
}
// ========== React 19:ref 作为普通 prop(不再需要 forwardRef) ==========
// React 19+
function MyInput({ ref, ...props }) {
return <input ref={ref} {...props} />
}
// 直接使用,无需 forwardRef 包裹
💡 面试加分点:
useImperativeHandle遵循最小暴露原则 ------不要直接暴露整个 DOM 节点,只暴露必要的方法。React 19 将ref作为普通 prop 传递,forwardRef将逐步废弃。
11. React 的组件复用模式对比?
| 模式 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 自定义 Hook | 函数中封装状态逻辑 | ✅ 最简洁、无嵌套 | 只能复用逻辑,不能复用 UI |
| HOC | 接受组件返回新组件 | 复用逻辑+UI 包装 | 嵌套地狱、props 冲突、难调试 |
| Render Props | 通过函数 prop 传递渲染逻辑 | 灵活 | 嵌套回调 |
| 组合组件 | children/Slot 模式 | ✅ 直观 | 适用场景有限 |
jsx
// ========== 1. HOC(高阶组件) ==========
const withAuth = (WrappedComponent, requiredRole) => {
return function AuthComponent(props) {
const { user } = useAuth()
if (!user) return <Navigate to="/login" />
if (requiredRole && !user.roles.includes(requiredRole)) return <div>没有权限</div>
return <WrappedComponent {...props} user={user} />
}
}
const withLoading = (WrappedComponent) => {
return function LoadingComponent({ isLoading, ...props }) {
if (isLoading) return <div className="spinner">Loading...</div>
return <WrappedComponent {...props} />
}
}
// ⚠️ HOC 嵌套地狱
const AdminPage = withAuth(withLoading(withLogger(AdminContent)), 'admin')
// ========== 2. Render Props ==========
const MouseTracker = ({ render }) => {
const [pos, setPos] = useState({ x: 0, y: 0 })
const handleMove = (e) => setPos({ x: e.clientX, y: e.clientY })
return <div onMouseMove={handleMove}>{render(pos)}</div>
}
// <MouseTracker render={({ x, y }) => <p>鼠标位置:{x}, {y}</p>} />
// ========== 3. 自定义 Hook(推荐替代 HOC 和 Render Props) ==========
function useMousePosition() {
const [pos, setPos] = useState({ x: 0, y: 0 })
useEffect(() => {
const handler = (e) => setPos({ x: e.clientX, y: e.clientY })
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
return pos
}
// 使用:简洁明了,无嵌套
const Component = () => {
const { x, y } = useMousePosition()
return <p>鼠标位置:{x}, {y}</p>
}
💡 面试加分点: 现代 React 中 自定义 Hook 几乎完全取代了 HOC 和 Render Props------逻辑更清晰、没有嵌套地狱、没有 props 命名冲突。HOC 唯一还有用的场景是「需要在不修改组件的情况下包装它」(如路由守卫、权限控制等跨切面逻辑)。
12. React 中的状态管理方案对比
| 方案 | 学习曲线 | 包体积 | 适用场景 | 特点 |
|---|---|---|---|---|
| useState + Context | 低 | 0 | 小型应用 | 内置,但 Context 性能差 |
| useReducer + Context | 低 | 0 | 中小型应用 | dispatch 稳定引用 |
| Redux Toolkit | 中 | 大 | 大型团队项目 | 生态完善、DevTools 强 |
| Zustand | ✅ 低 | ✅ 小 (2KB) | 中小型应用 | 极简、hooks 原生 |
| Jotai | 低 | 小 | 原子化状态 | 类似 Recoil,更轻量 |
| TanStack Query | 中 | 中 | 服务端状态 | 缓存/重试/轮询 |
jsx
// ========== 1. useReducer + Context(小型应用) ==========
const AppContext = createContext(null)
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState)
const value = useMemo(() => ({ state, dispatch }), [state])
return <AppContext.Provider value={value}>{children}</AppContext.Provider>
}
// ========== 2. Redux Toolkit(中大型应用) ==========
import { createSlice, configureStore } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value++ }, // Immer:可以直接"修改"state
decrement: (state) => { state.value-- },
incrementByAmount: (state, action) => { state.value += action.payload },
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
const store = configureStore({
reducer: { counter: counterSlice.reducer }
})
// 组件中使用
import { useSelector, useDispatch } from 'react-redux'
const Counter = () => {
const count = useSelector(state => state.counter.value)
const dispatch = useDispatch()
return (
<div>
<p>{count}</p>
<button onClick={() => dispatch(increment())}>+</button>
</div>
)
}
// ========== 3. Zustand(推荐,极简) ==========
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
const useCounterStore = create(
devtools(
persist(
(set, get) => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
// 异步 action
fetchCount: async () => {
const res = await fetch('/api/count')
const data = await res.json()
set({ count: data.count })
},
// 通过 get() 访问当前状态
doubleCount: () => get().count * 2,
}),
{ name: 'counter-storage' } // persist 到 localStorage
)
)
)
const Counter = () => {
const count = useCounterStore(state => state.count) // ✅ 选择性订阅,精确更新
const increment = useCounterStore(state => state.increment)
return <button onClick={increment}>{count}</button>
}
// ========== 4. Jotai(原子化状态) ==========
import { atom, useAtom } from 'jotai'
const countAtom = atom(0)
const doubleAtom = atom(get => get(countAtom) * 2) // 派生原子
const Counter = () => {
const [count, setCount] = useAtom(countAtom)
const [double] = useAtom(doubleAtom)
return <p>{count} × 2 = {double}</p>
}
💡 面试加分点: 状态管理选型原则:服务端状态用 TanStack Query/SWR (缓存、重试、乐观更新),客户端全局状态用 Zustand (简单、无 Provider、精确更新),大型团队用 Redux Toolkit(规范化、中间件生态、DevTools)。Context 适合低频变化的数据(主题、语言、认证信息)。
13. React Router v6 完整指南
路由配置方式
jsx
import { createBrowserRouter, RouterProvider, Outlet, Link, NavLink } from 'react-router-dom'
// ========== 方式一:对象式配置(推荐) ==========
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
errorElement: <ErrorPage />, // 路由错误处理
children: [
{ index: true, element: <Home /> }, // 默认子路由
{ path: 'about', element: <About /> },
{
path: 'users',
element: <UserLayout />,
children: [
{ index: true, element: <UserList /> },
{ path: ':userId', element: <UserDetail /> }, // 动态路由
{ path: ':userId/edit', element: <UserEdit /> },
]
},
{
path: 'dashboard',
lazy: () => import('./pages/Dashboard'), // ✅ 路由懒加载(v6.4+)
},
{ path: '*', element: <NotFound /> }, // 404
]
}
])
const App = () => <RouterProvider router={router} />
// ========== 方式二:JSX 配置 ==========
import { BrowserRouter, Routes, Route } from 'react-router-dom'
const App = () => (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="users/:userId" element={<UserDetail />} />
</Route>
</Routes>
</BrowserRouter>
)
// ========== Layout 组件(使用 Outlet 渲染子路由) ==========
const Layout = () => (
<div>
<nav>
<NavLink to="/" className={({ isActive }) => isActive ? 'active' : ''}>首页</NavLink>
<NavLink to="/about">关于</NavLink>
<NavLink to="/users">用户</NavLink>
</nav>
<main>
<Outlet /> {/* 子路由渲染在这里 */}
</main>
</div>
)
路由 Hooks
jsx
import {
useParams, // 获取动态路由参数
useSearchParams, // 获取/设置查询参数
useNavigate, // 编程式导航
useLocation, // 当前 location 对象
useMatch, // 匹配指定路径
useOutletContext, // 获取父路由 Outlet 传递的 context
} from 'react-router-dom'
const UserDetail = () => {
const { userId } = useParams() // /users/123 → { userId: '123' }
const [searchParams, setSearchParams] = useSearchParams() // ?tab=posts
const navigate = useNavigate()
const location = useLocation() // { pathname, search, hash, state }
const tab = searchParams.get('tab') || 'profile'
return (
<div>
<h1>用户 {userId}</h1>
<button onClick={() => setSearchParams({ tab: 'posts' })}>文章</button>
<button onClick={() => navigate(-1)}>返回</button>
<button onClick={() => navigate('/users', { replace: true })}>回列表(replace)</button>
<button onClick={() => navigate('/login', { state: { from: location } })}>
登录(携带来源信息)
</button>
</div>
)
}
路由三种模式
| 模式 | API | URL 示例 | 特点 |
|---|---|---|---|
| BrowserRouter | createBrowserRouter |
/users/123 |
HTML5 History,需服务器配置 |
| HashRouter | createHashRouter |
/#/users/123 |
兼容旧浏览器,无需服务器配置 |
| MemoryRouter | createMemoryRouter |
无 URL 变化 | 测试/非浏览器环境使用 |
💡 面试加分点: React Router v6 的重大变化:①
<Switch>改为<Routes>;②component/render改为element(直接传 JSX);③ 移除了exact(默认精确匹配);④useHistory改为useNavigate;⑤ 新增loader/action数据路由 API(类似 Remix)。
14. React Router 路由守卫和权限控制
React Router 没有内置的路由守卫(不像 Vue Router),需要自己实现。
jsx
// ========== 1. 高阶组件式路由守卫 ==========
const ProtectedRoute = ({ children, requiredRoles }) => {
const { user, isLoading } = useAuth()
const location = useLocation()
if (isLoading) return <div>加载中...</div>
if (!user) {
// 未登录:重定向到登录页,记录来源
return <Navigate to="/login" state={{ from: location }} replace />
}
if (requiredRoles && !requiredRoles.some(role => user.roles.includes(role))) {
// 无权限
return <Navigate to="/403" replace />
}
return children
}
// 使用(路由配置中)
const router = createBrowserRouter([
{
path: '/dashboard',
element: (
<ProtectedRoute requiredRoles={['admin']}>
<Dashboard />
</ProtectedRoute>
)
}
])
// ========== 2. 使用 loader 做路由守卫(v6.4+ 推荐) ==========
const router = createBrowserRouter([
{
path: '/dashboard',
loader: async () => {
const user = await getUser()
if (!user) throw redirect('/login')
if (!user.roles.includes('admin')) throw redirect('/403')
return { user }
},
element: <Dashboard />,
}
])
// 在组件中使用 loader 返回的数据
import { useLoaderData } from 'react-router-dom'
const Dashboard = () => {
const { user } = useLoaderData()
return <h1>欢迎,{user.name}</h1>
}
// ========== 3. 登录后重定向回原页面 ==========
const LoginPage = () => {
const navigate = useNavigate()
const location = useLocation()
const from = location.state?.from?.pathname || '/'
const handleLogin = async (credentials) => {
await login(credentials)
navigate(from, { replace: true }) // 登录成功后跳回原页面
}
return <LoginForm onSubmit={handleLogin} />
}
// ========== 4. 路由元数据(自定义 handle) ==========
const router = createBrowserRouter([
{
path: '/admin',
element: <Admin />,
handle: {
crumb: () => '管理后台', // 面包屑
title: '管理后台', // 页面标题
requireAuth: true,
},
children: [
{
path: 'users',
element: <AdminUsers />,
handle: { crumb: () => '用户管理', title: '用户管理' }
}
]
}
])
// 读取路由元数据
import { useMatches } from 'react-router-dom'
const Breadcrumb = () => {
const matches = useMatches()
const crumbs = matches.filter(m => m.handle?.crumb).map(m => m.handle.crumb())
return <nav>{crumbs.join(' > ')}</nav>
}
💡 面试加分点: React Router v6.4 引入的
loader+action是借鉴 Remix 的数据路由------先加载数据再渲染组件 ,避免了组件内部 useEffect 请求造成的瀑布流。路由守卫推荐用loader+redirect实现,比高阶组件更优雅。
15. Redux Toolkit 深入使用
createAsyncThunk(异步 Action)
jsx
import { createSlice, createAsyncThunk, configureStore } from '@reduxjs/toolkit'
// ========== 1. 定义异步 Thunk ==========
const fetchUsers = createAsyncThunk(
'users/fetchUsers',
async ({ page, pageSize }, { rejectWithValue, getState }) => {
try {
const response = await fetch(`/api/users?page=${page}&size=${pageSize}`)
if (!response.ok) throw new Error('请求失败')
return await response.json()
} catch (error) {
return rejectWithValue(error.message) // 自定义错误信息
}
},
{
// 条件执行:如果已经在加载中,不重复请求
condition: (_, { getState }) => {
const { users } = getState()
return users.status !== 'loading'
}
}
)
const deleteUser = createAsyncThunk('users/delete', async (userId, { dispatch }) => {
await fetch(`/api/users/${userId}`, { method: 'DELETE' })
dispatch(fetchUsers({ page: 1, pageSize: 10 })) // 删除后刷新列表
return userId
})
// ========== 2. Slice 处理异步状态 ==========
const usersSlice = createSlice({
name: 'users',
initialState: {
list: [],
total: 0,
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
},
reducers: {
clearUsers: (state) => { state.list = []; state.total = 0 },
},
// extraReducers 处理 createAsyncThunk 生成的 action
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.status = 'loading'
state.error = null
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded'
state.list = action.payload.data
state.total = action.payload.total
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed'
state.error = action.payload || action.error.message
})
}
})
// ========== 3. 组件中使用 ==========
const UserList = () => {
const dispatch = useDispatch()
const { list, status, error, total } = useSelector(state => state.users)
useEffect(() => {
dispatch(fetchUsers({ page: 1, pageSize: 10 }))
}, [dispatch])
if (status === 'loading') return <Spinner />
if (status === 'failed') return <ErrorMsg message={error} />
return (
<ul>
{list.map(user => (
<li key={user.id}>
{user.name}
<button onClick={() => dispatch(deleteUser(user.id))}>删除</button>
</li>
))}
</ul>
)
}
RTK Query(数据请求利器)
jsx
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
// ========== 定义 API ==========
const userApi = createApi({
reducerPath: 'userApi',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) headers.set('Authorization', `Bearer ${token}`)
return headers
}
}),
tagTypes: ['User'], // 缓存标签
endpoints: (builder) => ({
// 查询
getUsers: builder.query({
query: (params) => ({ url: '/users', params }),
providesTags: (result) =>
result ? [...result.map(({ id }) => ({ type: 'User', id })), 'User'] : ['User'],
}),
getUserById: builder.query({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [{ type: 'User', id }],
}),
// 修改
addUser: builder.mutation({
query: (newUser) => ({ url: '/users', method: 'POST', body: newUser }),
invalidatesTags: ['User'], // 添加后自动刷新列表
}),
updateUser: builder.mutation({
query: ({ id, ...data }) => ({ url: `/users/${id}`, method: 'PUT', body: data }),
invalidatesTags: (result, error, { id }) => [{ type: 'User', id }],
}),
deleteUser: builder.mutation({
query: (id) => ({ url: `/users/${id}`, method: 'DELETE' }),
invalidatesTags: (result, error, id) => [{ type: 'User', id }],
}),
}),
})
export const {
useGetUsersQuery, useGetUserByIdQuery,
useAddUserMutation, useUpdateUserMutation, useDeleteUserMutation
} = userApi
// ========== 组件中使用 ==========
const UserList = () => {
const { data: users, isLoading, error, refetch } = useGetUsersQuery({ page: 1 })
const [deleteUser, { isLoading: isDeleting }] = useDeleteUserMutation()
if (isLoading) return <Spinner />
if (error) return <ErrorMsg />
return (
<ul>
{users?.map(user => (
<li key={user.id}>
{user.name}
<button onClick={() => deleteUser(user.id)} disabled={isDeleting}>删除</button>
</li>
))}
</ul>
)
}
// store 配置
const store = configureStore({
reducer: {
[userApi.reducerPath]: userApi.reducer,
users: usersSlice.reducer,
},
middleware: (getDefault) => getDefault().concat(userApi.middleware),
})
💡 面试加分点: RTK Query 类似 TanStack Query------自动缓存、自动去重、自动刷新、乐观更新。
invalidatesTags+providesTags实现了声明式缓存失效------修改数据后自动刷新相关查询。与 TanStack Query 不同的是,RTK Query 内置在 Redux 中,适合已使用 Redux 的项目。
16. React 的合成事件(SyntheticEvent)
| 对比项 | 原生 DOM 事件 | React 合成事件 |
|---|---|---|
| 事件绑定 | 绑定到具体 DOM | 委托到 root 节点(React 17+) |
| 事件对象 | 原生 Event | SyntheticEvent(跨浏览器包装) |
| 事件池 | --- | React 16 有事件池(17 移除) |
| 阻止冒泡 | e.stopPropagation() |
只阻止 React 事件冒泡 |
| 命名 | onclick |
onClick(驼峰) |
jsx
const EventDemo = () => {
// ========== 事件处理 ==========
const handleClick = (e) => {
console.log(e) // SyntheticEvent(合成事件对象)
console.log(e.nativeEvent) // 原生事件对象
console.log(e.target) // 触发事件的元素
console.log(e.currentTarget) // 绑定事件的元素
}
// ========== 事件传参 ==========
const handleDelete = (id, e) => {
e.stopPropagation()
console.log('删除:', id)
}
// ========== 表单事件 ==========
const handleSubmit = (e) => {
e.preventDefault() // 阻止表单默认提交
// 处理提交逻辑
}
return (
<form onSubmit={handleSubmit}>
<button onClick={handleClick}>点击</button>
<button onClick={(e) => handleDelete(123, e)}>删除</button>
</form>
)
}
// ========== React 事件委托变化 ==========
// React 16:事件委托到 document
// React 17+:事件委托到 root 节点(createRoot 挂载点)
// 好处:多个 React 应用共存时不会互相干扰
// ========== 合成事件和原生事件的执行顺序 ==========
// 1. 原生事件(捕获阶段)
// 2. 原生事件(冒泡阶段)
// 3. React 合成事件(捕获阶段)
// 4. React 合成事件(冒泡阶段)
// 注意:React 17+ 将合成事件绑定到 root 而不是 document,所以顺序略有变化
// ⚠️ 混用原生事件和合成事件时,e.stopPropagation() 只阻止同类型事件
// React 的 stopPropagation 不会阻止原生事件冒泡
💡 面试加分点: React 17 把事件委托从
document改到了根节点 ------这是为了支持微前端等多个 React 实例共存的场景。React 的e.stopPropagation()只能阻止 React 合成事件冒泡,不能阻止原生事件冒泡------如果需要,得用e.nativeEvent.stopImmediatePropagation()。
17. 受控组件 vs 非受控组件
| 对比项 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据管理 | React state 驱动 | DOM 自身管理 |
| 取值方式 | value + onChange |
ref.current.value |
| 实时验证 | ✅ 每次输入都可验证 | ❌ 需要手动触发 |
| 性能 | 每次输入触发重渲染 | ✅ 不触发重渲染 |
| 推荐场景 | 表单验证、联动 | 文件上传、简单表单 |
jsx
// ========== 受控组件 ==========
const ControlledForm = () => {
const [form, setForm] = useState({ name: '', email: '', age: '' })
const [errors, setErrors] = useState({})
const handleChange = (e) => {
const { name, value } = e.target
setForm(prev => ({ ...prev, [name]: value }))
// ✅ 实时验证
if (name === 'email' && value && !/\S+@\S+\.\S+/.test(value)) {
setErrors(prev => ({ ...prev, email: '邮箱格式不正确' }))
} else {
setErrors(prev => ({ ...prev, [name]: '' }))
}
}
const handleSubmit = (e) => {
e.preventDefault()
console.log(form)
}
return (
<form onSubmit={handleSubmit}>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
{errors.email && <span className="error">{errors.email}</span>}
<button type="submit">提交</button>
</form>
)
}
// ========== 非受控组件 ==========
const UncontrolledForm = () => {
const nameRef = useRef()
const fileRef = useRef()
const handleSubmit = (e) => {
e.preventDefault()
console.log('Name:', nameRef.current.value)
console.log('File:', fileRef.current.files[0])
}
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} defaultValue="默认值" /> {/* defaultValue 而非 value */}
<input ref={fileRef} type="file" /> {/* 文件输入只能非受控 */}
<button type="submit">提交</button>
</form>
)
}
// ========== 推荐:使用表单库(React Hook Form) ==========
import { useForm } from 'react-hook-form'
const FormWithLib = () => {
const { register, handleSubmit, formState: { errors } } = useForm()
const onSubmit = (data) => console.log(data)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name', { required: '姓名必填' })} />
{errors.name && <span>{errors.name.message}</span>}
<input {...register('email', {
required: '邮箱必填',
pattern: { value: /\S+@\S+\.\S+/, message: '邮箱格式不正确' }
})} />
{errors.email && <span>{errors.email.message}</span>}
<button type="submit">提交</button>
</form>
)
}
💡 面试加分点:
<input type="file">只能是非受控组件(无法通过 React 设置 value)。生产中推荐 React Hook Form------它基于非受控组件 + ref,性能比 Formik(受控组件)好很多,表单字段变化不会触发整个表单重渲染。
18. React 组件的生命周期(类组件 vs 函数组件)
kotlin
类组件生命周期:
┌─────────────────────────────────────────────────┐
│ 挂载阶段 │
│ constructor → getDerivedStateFromProps → │
│ render → componentDidMount │
├─────────────────────────────────────────────────┤
│ 更新阶段 │
│ getDerivedStateFromProps → shouldComponentUpdate│
│ → render → getSnapshotBeforeUpdate → │
│ componentDidUpdate │
├─────────────────────────────────────────────────┤
│ 卸载阶段 │
│ componentWillUnmount │
├─────────────────────────────────────────────────┤
│ 错误处理 │
│ getDerivedStateFromError → componentDidCatch │
└─────────────────────────────────────────────────┘
| 类组件生命周期 | 函数组件 Hook 对应 |
|---|---|
| constructor | useState 惰性初始化 |
| getDerivedStateFromProps | useState + 渲染期间更新 |
| render | 函数体本身 |
| componentDidMount | useEffect(() => {}, []) |
| componentDidUpdate | useEffect(() => {}, [deps]) |
| componentWillUnmount | useEffect 返回的清理函数 |
| shouldComponentUpdate | React.memo |
| getSnapshotBeforeUpdate | 无直接对应(用 useLayoutEffect) |
| componentDidCatch | 无(仍需类组件) |
jsx
// ========== 函数组件等价实现 ==========
const FunctionalLifecycle = ({ id }) => {
// constructor 等价
const [data, setData] = useState(() => expensiveInit())
// componentDidMount 等价
useEffect(() => {
console.log('挂载')
return () => console.log('卸载') // componentWillUnmount 等价
}, [])
// componentDidUpdate 等价(跳过首次)
const isFirst = useRef(true)
useEffect(() => {
if (isFirst.current) {
isFirst.current = false
return
}
console.log('id 更新为:', id)
}, [id])
// render
return <div>{data}</div>
}
💡 面试加分点: React 16 废弃了
componentWillMount、componentWillReceiveProps、componentWillUpdate(加了 UNSAFE_ 前缀),因为它们在 Fiber 的 Render 阶段可能被多次调用。函数组件 + Hooks 是 React 的未来,唯一还需要类组件的地方是错误边界。
19. React 中的 CSS 方案对比
| 方案 | 作用域 | 运行时 | TypeScript | 优缺点 |
|---|---|---|---|---|
| CSS Modules | ✅ 局部 | ❌ 无 | ⚠️ 需声明 | 零运行时,构建时处理 |
| Tailwind CSS | ✅ 原子类 | ❌ 无 | ✅ | 开发快,class 长 |
| styled-components | ✅ 局部 | ⚠️ 有 | ✅ | 动态样式,SSR 需配置 |
| Emotion | ✅ 局部 | ⚠️ 有 | ✅ | 类似 SC,更灵活 |
| CSS-in-JS (零运行时) | ✅ 局部 | ❌ 无 | ✅ | vanilla-extract、Panda CSS |
| 内联样式 | ✅ | --- | ✅ | 无伪类/媒体查询 |
jsx
// ========== 1. CSS Modules ==========
// Button.module.css → .btn { color: red; } .btnPrimary { color: blue; }
import styles from './Button.module.css'
const Button = () => <button className={styles.btn}>按钮</button>
// 生成:<button class="Button_btn_x3j2k">
// ========== 2. Tailwind CSS ==========
const Button = () => (
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
按钮
</button>
)
// ========== 3. styled-components ==========
import styled from 'styled-components'
const StyledButton = styled.button`
background: ${props => props.$primary ? 'blue' : 'gray'};
color: white;
padding: 8px 16px;
border-radius: 4px;
&:hover { opacity: 0.8; }
`
const App = () => <StyledButton $primary>按钮</StyledButton>
// ========== 4. clsx/classnames 条件类名 ==========
import clsx from 'clsx'
const Button = ({ variant, size, disabled }) => (
<button className={clsx(
'btn',
variant === 'primary' && 'btn-primary',
variant === 'danger' && 'btn-danger',
size === 'large' && 'btn-lg',
disabled && 'btn-disabled'
)}>
按钮
</button>
)
💡 面试加分点: 2024-2025 的趋势:Tailwind CSS + CSS Modules 组合最流行------Tailwind 处理大部分样式,CSS Modules 处理复杂/动态样式。CSS-in-JS 运行时方案(SC/Emotion)因性能问题(运行时生成样式)逐渐被零运行时方案(vanilla-extract、Panda CSS)替代。
20. React 19 新特性
| 特性 | 说明 |
|---|---|
| React Compiler | 自动记忆化,不再需要手写 useMemo/useCallback |
| Actions | 表单处理新范式(useActionState、useFormStatus) |
| use() | 在条件/循环中读取 Promise 和 Context |
| ref 作为 prop | 不再需要 forwardRef |
| 文档元数据 | 组件中直接写 <title>、<meta> |
| 样式支持 | <link> 样式优先级/去重 |
| Server Components | RSC 正式稳定 |
jsx
// ========== 1. use() Hook:在条件中读取 Promise/Context ==========
import { use, Suspense } from 'react'
function UserProfile({ userPromise }) {
// 可以在 if/循环中使用(打破了 Hook 规则限制)
const user = use(userPromise) // 自动配合 Suspense
return <h1>{user.name}</h1>
}
// 条件读取 Context
function ThemeText({ showTheme }) {
if (showTheme) {
const theme = use(ThemeContext) // ✅ 可以在条件中使用
return <p>当前主题: {theme}</p>
}
return <p>主题已隐藏</p>
}
// ========== 2. useActionState(表单处理) ==========
import { useActionState } from 'react'
function LoginForm() {
const [state, formAction, isPending] = useActionState(
async (prevState, formData) => {
const email = formData.get('email')
const password = formData.get('password')
try {
await login(email, password)
return { success: true }
} catch (error) {
return { error: error.message }
}
},
{ error: null, success: false }
)
return (
<form action={formAction}>
<input name="email" type="email" />
<input name="password" type="password" />
{state.error && <p className="error">{state.error}</p>}
<button disabled={isPending}>{isPending ? '登录中...' : '登录'}</button>
</form>
)
}
// ========== 3. useFormStatus(子组件获取表单状态) ==========
import { useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending, data, method } = useFormStatus()
return <button disabled={pending}>{pending ? '提交中...' : '提交'}</button>
}
// ========== 4. useOptimistic(乐观更新) ==========
import { useOptimistic } from 'react'
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
)
const handleAdd = async (formData) => {
const newTodo = { text: formData.get('text'), id: Date.now() }
addOptimistic(newTodo) // 立即显示(乐观更新)
await addTodo(newTodo) // 实际请求
}
return (
<div>
<form action={handleAdd}>
<input name="text" />
<button type="submit">添加</button>
</form>
{optimisticTodos.map(todo => (
<p key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>{todo.text}</p>
))}
</div>
)
}
// ========== 5. ref 作为普通 prop ==========
// React 19 之前
const Input = forwardRef((props, ref) => <input ref={ref} {...props} />)
// React 19
function Input({ ref, ...props }) {
return <input ref={ref} {...props} />
}
// ========== 6. 文档元数据 ==========
function BlogPost({ post }) {
return (
<article>
<title>{post.title}</title>
<meta name="description" content={post.summary} />
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
💡 面试加分点: React 19 最大的变革是 React Compiler ------它在编译时自动完成记忆化(等效于 useMemo/useCallback/memo),开发者不再需要手动优化。
use()打破了「Hook 不能在条件/循环中调用」的限制。useActionState是 React 官方对表单处理的标准答案。
21. TanStack Query(React Query)
TanStack Query 是管理服务端状态(Server State)的最佳方案。
| 特性 | 说明 |
|---|---|
| 自动缓存 | 相同 queryKey 的请求自动复用缓存 |
| 后台刷新 | 窗口聚焦时自动重新请求 |
| 智能重试 | 请求失败自动重试(默认 3 次) |
| 乐观更新 | 先更新 UI,失败后回滚 |
| 分页/无限滚动 | 内置 useInfiniteQuery |
| 并行/依赖查询 | 自动处理查询依赖关系 |
jsx
import { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider } from '@tanstack/react-query'
// ========== 配置 ==========
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 分钟内不重新请求
gcTime: 10 * 60 * 1000, // 10 分钟后清除缓存(原 cacheTime)
retry: 2, // 失败重试 2 次
refetchOnWindowFocus: true, // 窗口聚焦时刷新
}
}
})
const App = () => (
<QueryClientProvider client={queryClient}><Root /></QueryClientProvider>
)
// ========== 查询 ==========
function UserList() {
const { data, isLoading, isError, error, refetch, isFetching } = useQuery({
queryKey: ['users', { page: 1 }], // 唯一标识(自动序列化)
queryFn: ({ queryKey }) => fetch(`/api/users?page=${queryKey[1].page}`).then(r => r.json()),
staleTime: 30 * 1000, // 30 秒内不重新请求
placeholderData: previousData, // 切换参数时保留旧数据
})
if (isLoading) return <Spinner />
if (isError) return <p>错误: {error.message}</p>
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
// ========== 依赖查询 ==========
function UserPosts({ userId }) {
const userQuery = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
})
const postsQuery = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId),
enabled: !!userQuery.data, // 等用户数据加载完再查询文章
})
}
// ========== 修改(乐观更新) ==========
function useUpdateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (updatedUser) => fetch(`/api/users/${updatedUser.id}`, {
method: 'PUT', body: JSON.stringify(updatedUser)
}),
// 乐观更新
onMutate: async (newUser) => {
await queryClient.cancelQueries({ queryKey: ['users'] })
const previous = queryClient.getQueryData(['users'])
queryClient.setQueryData(['users'], (old) =>
old.map(u => u.id === newUser.id ? { ...u, ...newUser } : u)
)
return { previous } // 返回 context
},
onError: (err, newUser, context) => {
queryClient.setQueryData(['users'], context.previous) // 回滚
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['users'] }) // 无论成败都刷新
}
})
}
// ========== 无限滚动 ==========
import { useInfiniteQuery } from '@tanstack/react-query'
function InfiniteList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor || undefined,
})
return (
<div>
{data?.pages.map(page => page.items.map(item => <div key={item.id}>{item.title}</div>))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? '加载中...' : '加载更多'}
</button>
)}
</div>
)
}
💡 面试加分点: TanStack Query 解决了前端最大的痛点------服务端状态管理 (缓存、去重、后台刷新、乐观更新、分页)。与 Redux 的区别:Redux 管理客户端状态 (UI 状态、用户偏好),TanStack Query 管理服务端状态 (API 数据)。
queryKey是唯一标识,改变它等于发起新请求。
22. SSR 和 Next.js
| 渲染方式 | 说明 | 首屏速度 | SEO | 适用场景 |
|---|---|---|---|---|
| CSR | 客户端渲染 | 慢 | ❌ | 后台管理系统 |
| SSR | 服务端渲染 | ✅ 快 | ✅ | 动态内容(电商、新闻) |
| SSG | 静态生成 | ✅✅ 最快 | ✅ | 博客、文档、营销页 |
| ISR | 增量静态生成 | ✅✅ 快 | ✅ | 数据定期更新的页面 |
| RSC | React Server Components | ✅✅ | ✅ | 数据密集型页面 |
jsx
// ========== Next.js App Router(推荐) ==========
// app/page.tsx → /
// app/about/page.tsx → /about
// app/users/[id]/page.tsx → /users/:id
// app/blog/[...slug]/page.tsx → /blog/a/b/c
// ========== Server Component(默认,在服务端渲染) ==========
// app/users/page.tsx
async function UsersPage() {
// 直接在组件中 await(无需 useEffect!)
const users = await fetch('https://api.example.com/users').then(r => r.json())
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
)
}
// Server Component 优势:
// ✅ 不发送 JS 到客户端(减小包体积)
// ✅ 直接访问数据库/文件系统
// ✅ 自动代码分割
// ========== Client Component(交互组件) ==========
'use client' // 必须声明
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
// ========== 数据获取模式 ==========
// Server Component 中直接 async/await
async function Page({ params }) {
const post = await db.post.findUnique({ where: { id: params.id } })
return <article>{post.title}</article>
}
// ========== Metadata(SEO) ==========
// app/page.tsx
export const metadata = {
title: '首页',
description: '网站描述',
openGraph: { title: '分享标题', image: '/og.png' },
}
// 动态 Metadata
export async function generateMetadata({ params }) {
const post = await getPost(params.id)
return { title: post.title, description: post.excerpt }
}
// ========== 路由分组和布局 ==========
// app/(marketing)/layout.tsx → 营销页面布局
// app/(dashboard)/layout.tsx → 后台布局
// (xxx) 括号目录不影响 URL
// ========== Server Actions ==========
// app/actions.ts
'use server'
export async function createUser(formData) {
const name = formData.get('name')
await db.user.create({ data: { name } })
revalidatePath('/users') // 重新验证页面缓存
}
// app/create-user/page.tsx
import { createUser } from './actions'
export default function CreateUserPage() {
return (
<form action={createUser}>
<input name="name" />
<button type="submit">创建</button>
</form>
)
}
💡 面试加分点: Next.js App Router 引入的 React Server Components (RSC) 是 React 的范式转变------组件默认在服务端渲染,只有需要交互的组件才添加
'use client'。Server Component 的 JS 不会发送到客户端,可以直接访问数据库------这让 React 从纯前端框架变成了全栈框架。
23. React 的闭包陷阱详解
什么是闭包陷阱?
函数组件每次渲染都会创建新的闭包,如果闭包中捕获了旧的 state/props 值,就会出现"读到旧值"的问题。这是 React 函数组件最常见的 Bug。
jsx
// ========== 经典闭包陷阱:setTimeout 读到旧值 ==========
function Counter() {
const [count, setCount] = useState(0)
const handleClick = () => {
// ❌ 闭包陷阱!
setTimeout(() => {
// 这个 count 是点击时的值(闭包捕获),不是 3 秒后的最新值!
console.log('count:', count) // 永远打印点击那一刻的 count
setCount(count + 1) // 基于旧值更新,可能不对
}, 3000)
}
// 场景:count=0,快速点击 3 次
// 预期:0, 1, 2 → count = 3
// 实际:0, 0, 0 → count = 1(三次 setTimeout 都捕获了 count=0)
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>+1 (3秒后)</button>
</div>
)
}
解决方案大全
jsx
// ========== 解决方案1:函数式更新(解决 setState 读旧值) ==========
function Counter() {
const [count, setCount] = useState(0)
const handleClick = () => {
setTimeout(() => {
// ✅ 函数式更新:不依赖闭包中的 count,而是基于最新 state 更新
setCount(prev => prev + 1)
}, 3000)
}
// 快速点击 3 次 → count = 3 ✅
return <button onClick={handleClick}>{count}</button>
}
// ========== 解决方案2:useRef 读最新值(解决"读"旧值的问题) ==========
function Counter() {
const [count, setCount] = useState(0)
const countRef = useRef(count)
// 每次渲染同步 ref
useEffect(() => {
countRef.current = count
})
const handleClick = () => {
setTimeout(() => {
// ✅ ref.current 永远是最新值
console.log('最新 count:', countRef.current)
}, 3000)
}
return <button onClick={handleClick}>{count}</button>
}
// ========== 解决方案3:useRef 保存回调(避免 useEffect 依赖过多) ==========
function useInterval(callback, delay) {
const savedCallback = useRef(callback)
// 每次渲染更新 ref(不需要重启定时器)
useEffect(() => {
savedCallback.current = callback
})
useEffect(() => {
const id = setInterval(() => {
savedCallback.current() // ✅ 永远调用最新的回调
}, delay)
return () => clearInterval(id)
}, [delay]) // ✅ 只在 delay 变化时重启定时器
}
常见闭包陷阱场景
jsx
// ========== 场景1:useEffect 依赖遗漏 ==========
function SearchBox() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
useEffect(() => {
// ❌ 闭包陷阱:如果不加 query 到依赖数组
// 这个 effect 只在 mount 时运行一次,query 永远是 ''
const handler = setTimeout(() => {
fetchResults(query).then(setResults)
}, 300)
return () => clearTimeout(handler)
}, [query]) // ✅ 必须加上 query
return <input value={query} onChange={e => setQuery(e.target.value)} />
}
// ========== 场景2:事件监听器捕获旧 state ==========
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
const [isTop, setIsTop] = useState(true)
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY)
// ❌ 这里的 scrollY 是 useEffect 执行时的旧值
// console.log(scrollY) // 永远是 0
// ✅ 用函数式更新或直接读 window.scrollY
setIsTop(window.scrollY < 100)
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, []) // 空依赖:只绑定一次,闭包捕获的是初始值
return <p>scrollY: {scrollY}, isTop: {String(isTop)}</p>
}
// ========== 场景3:useCallback 的依赖与闭包 ==========
function Parent() {
const [count, setCount] = useState(0)
// ❌ 空依赖:handleClick 永远捕获 count=0
const handleClick = useCallback(() => {
console.log(count) // 永远是 0
}, [])
// ✅ 方案1:加上 count 依赖(但每次 count 变化都创建新函数)
const handleClick1 = useCallback(() => {
console.log(count)
}, [count])
// ✅ 方案2:useRef(不需要 count 依赖,函数引用稳定)
const countRef = useRef(count)
countRef.current = count
const handleClick2 = useCallback(() => {
console.log(countRef.current) // 永远是最新值
}, [])
return <MemoChild onClick={handleClick2} />
}
闭包陷阱速查表
javascript
// ========== 闭包陷阱场景和解决方案速查 ==========
| 场景 | 问题 | 解决方案 |
|--------------------------|---------------------------|-----------------------------------|
| setTimeout/setInterval | 回调读到旧 state | 函数式 setState / useRef |
| useEffect 空依赖 | effect 内读到初始 state | 加依赖 / useRef |
| useCallback 空依赖 | 回调读到旧 state | 加依赖 / useRef |
| 事件监听器 | handler 读到旧 state | useRef / 函数式更新 |
| Promise/async | .then 中读到旧 state | useRef / 函数式更新 |
💡 面试加分点: 闭包陷阱的根本原因是函数组件的 Capture Value 特性 ------每次渲染都是一个独立的闭包快照。解决思路只有两个方向:1)不依赖闭包值 (函数式更新
setState(prev => ...));2)用 ref 逃逸闭包 (useRef的.current是可变引用,不受闭包影响)。React 19 的 React Compiler 可以自动处理部分闭包问题,但理解底层原理仍然是面试必考。
24. React 的批量更新(Batching)机制
什么是批量更新?
批量更新是指 React 将多次 setState 合并为一次渲染,避免不必要的中间状态渲染。
jsx
// ========== React 17 的批量更新行为 ==========
function Counter() {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
// ✅ React 17 事件处理函数中:自动批量(只渲染一次)
const handleClick = () => {
setCount(c => c + 1) // 不会立即渲染
setFlag(f => !f) // 不会立即渲染
// → 两次更新合并,组件只渲染一次 ✅
}
// ❌ React 17 在 setTimeout/Promise 中:不批量!(渲染两次)
const handleAsync = () => {
setTimeout(() => {
setCount(c => c + 1) // 立即渲染一次!
setFlag(f => !f) // 又渲染一次!
// → 组件渲染了两次 ❌
}, 0)
}
// ❌ React 17 在 fetch 回调中:不批量!
const handleFetch = () => {
fetch('/api/data').then(() => {
setCount(c => c + 1) // 渲染一次
setFlag(f => !f) // 又渲染一次
})
}
console.log('render') // 观察渲染次数
return <button onClick={handleClick}>{count}</button>
}
React 18 的自动批量更新(Automatic Batching)
jsx
// ========== React 18:所有地方都自动批量! ==========
import { createRoot } from 'react-dom/client'
// 前提:必须使用 createRoot(不是 ReactDOM.render)
const root = createRoot(document.getElementById('root'))
root.render(<App />)
function Counter() {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
// ✅ 事件处理函数:批量(React 17 也支持)
const handleClick = () => {
setCount(c => c + 1)
setFlag(f => !f)
// → 只渲染一次 ✅
}
// ✅ setTimeout:也批量了!(React 18 新特性)
const handleTimeout = () => {
setTimeout(() => {
setCount(c => c + 1)
setFlag(f => !f)
// → 只渲染一次 ✅(React 17 中会渲染两次)
}, 0)
}
// ✅ Promise:也批量了!
const handleFetch = () => {
fetch('/api/data').then(() => {
setCount(c => c + 1)
setFlag(f => !f)
// → 只渲染一次 ✅
})
}
// ✅ 原生事件监听器:也批量了!
useEffect(() => {
const handler = () => {
setCount(c => c + 1)
setFlag(f => !f)
// → 只渲染一次 ✅
}
document.addEventListener('click', handler)
return () => document.removeEventListener('click', handler)
}, [])
console.log('render')
return <button onClick={handleClick}>{count}</button>
}
flushSync:强制同步更新
jsx
// ========== 跳出批量更新:flushSync ==========
import { flushSync } from 'react-dom'
function Counter() {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
const handleClick = () => {
// 强制同步更新(不批量)
flushSync(() => {
setCount(c => c + 1)
})
// 此时 DOM 已经更新了!count 已经变了
console.log('DOM 已更新')
flushSync(() => {
setFlag(f => !f)
})
// 此时 DOM 又更新了一次
// 总共渲染了 2 次(而不是批量的 1 次)
}
// ========== flushSync 的实际应用场景 ==========
// 场景:需要在两次状态更新之间读取 DOM
const handleAdd = () => {
flushSync(() => {
setItems(prev => [...prev, newItem])
})
// DOM 已经更新,新元素已渲染
listRef.current.lastChild.scrollIntoView() // ✅ 滚动到新元素
}
return <button onClick={handleClick}>{count}</button>
}
批量更新原理(简化)
jsx
// ========== React 批量更新核心流程 ==========
// React 17:基于"执行上下文"标记
// 进入事件处理 → 设置 isBatchingUpdates = true
// setState → 收集到更新队列(不立即处理)
// 离开事件处理 → isBatchingUpdates = false → flush 队列 → 一次渲染
// 问题:setTimeout/Promise 的回调执行时,已经不在事件处理上下文中
// React 18:基于"优先级调度" + "自动包裹"
// 所有 setState 都进入 Fiber 的更新队列
// 在同一个微任务中的所有更新自动合并
// 通过 Scheduler 统一调度,一次处理所有待更新的 Fiber
// ========== 与 Vue 的批量更新对比 ==========
// Vue:基于响应式 + 异步更新队列
// - 数据变化 → watcher 入队(去重)→ nextTick flush
// - 从一开始就是自动批量的(任何场景)
// React 17:基于事件系统的"合成事件包裹"
// - 只在 React 事件处理函数中批量
// - setTimeout/Promise/原生事件中不批量
// React 18:基于 createRoot 的自动批量
// - 所有场景都自动批量(和 Vue 一致了)
// - 必须使用 createRoot(旧的 ReactDOM.render 不支持)
💡 面试加分点: React 18 的 Automatic Batching 是一个重大改进------将批量更新从"仅事件处理函数"扩展到"所有异步场景"。但前提是必须使用
createRootAPI (ReactDOM.render不支持)。flushSync是逃生舱口,用于需要立即同步 DOM 更新的场景(极少使用)。面试时还可以对比 Vue 的批量更新机制------Vue 从一开始就是全场景自动批量的(异步更新队列 + watcher 去重)。
25. React Suspense 的完整用法
Suspense 的核心概念
<Suspense> 是 React 提供的声明式异步 UI 边界------它让你"等待"某个异步操作完成,在等待期间显示 fallback 内容。
| 支持场景 | React 版本 | 说明 |
|---|---|---|
React.lazy 代码分割 |
16.6+ | 异步加载组件 |
| Data Fetching | 18+ | 配合支持 Suspense 的库(TanStack Query、Relay) |
| Server Components | 19+ | RSC 中的流式渲染 |
use() Hook |
19+ | 直接在组件中读取 Promise |
jsx
// ========== 1. 最基本的 Suspense:代码分割 ==========
import { lazy, Suspense } from 'react'
// lazy 返回一个"异步组件"
const Dashboard = lazy(() => import('./Dashboard'))
const Settings = lazy(() => import('./Settings'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
)
}
// ========== 2. 嵌套 Suspense ==========
function App() {
return (
// 外层 Suspense:页面级加载
<Suspense fallback={<PageSkeleton />}>
<Header />
<main>
{/* 内层 Suspense:模块级加载 */}
<Suspense fallback={<CardSkeleton />}>
<UserProfile />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<OrderList />
</Suspense>
</main>
</Suspense>
)
// 嵌套 Suspense 的好处:
// - 外层加载完毕后显示 Header + 两个骨架屏
// - UserProfile 和 OrderList 各自独立加载
// - 某个模块失败不影响其他模块
}
React 19 的 use() + Suspense
jsx
// ========== 3. use() + Suspense(React 19) ==========
import { use, Suspense } from 'react'
// ✅ use() 可以在条件中使用(打破 Hook 规则限制)
function UserProfile({ userPromise }) {
const user = use(userPromise) // 自动"挂起"直到 Promise resolve
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
// 父组件
function App() {
// ✅ 在父组件创建 Promise(不是在子组件内部)
const userPromise = fetchUser(userId)
return (
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
)
}
// ⚠️ 注意:不要在组件内部创建 Promise
function BadExample() {
// ❌ 每次渲染都创建新的 Promise → 无限循环!
const user = use(fetch('/api/user').then(r => r.json()))
return <div>{user.name}</div>
}
配合 TanStack Query 使用
jsx
// ========== 4. TanStack Query + Suspense ==========
import { useSuspenseQuery } from '@tanstack/react-query'
// ✅ useSuspenseQuery:数据加载期间自动挂起
function UserList() {
// 不需要处理 isLoading/isError,Suspense 和 ErrorBoundary 接管
const { data: users } = useSuspenseQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json())
})
// data 一定有值(不用判断 undefined)
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
// ========== 配合 ErrorBoundary ==========
import { ErrorBoundary } from 'react-error-boundary'
function App() {
return (
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<LoadingSpinner />}>
<UserList />
</Suspense>
</ErrorBoundary>
)
// 加载中 → <LoadingSpinner />
// 加载成功 → <UserList />
// 加载失败 → <ErrorMessage />(被 ErrorBoundary 捕获)
}
// ========== 5. SuspenseList(实验性,控制加载顺序) ==========
// React 未来可能稳定的 API
// <SuspenseList revealOrder="forwards">
// <Suspense fallback={<Skeleton1 />}><Section1 /></Suspense>
// <Suspense fallback={<Skeleton2 />}><Section2 /></Suspense>
// <Suspense fallback={<Skeleton3 />}><Section3 /></Suspense>
// </SuspenseList>
// revealOrder="forwards" → 按顺序展示(即使后面的先加载完)
// revealOrder="together" → 全部加载完后同时展示
Suspense 原理(简化)
jsx
// ========== Suspense 的底层机制 ==========
// 1. 组件"挂起"时,会 throw 一个 Promise
// 2. Suspense 捕获这个 Promise
// 3. 渲染 fallback
// 4. Promise resolve 后,重新渲染子组件
// 简化版工作流程:
// try {
// render(<AsyncComponent />)
// } catch (promise) {
// if (promise instanceof Promise) {
// // 渲染 fallback
// render(<Fallback />)
// // 等待 Promise 完成
// promise.then(() => {
// // 重新渲染子组件
// render(<AsyncComponent />)
// })
// }
// }
// 这就是为什么 use() 和 React.lazy 能配合 Suspense:
// - React.lazy:加载组件的 import() 返回 Promise,未完成时 throw
// - use(promise):Promise 未 resolve 时 throw
💡 面试加分点: Suspense 的本质是声明式的异步 UI 边界 ------把"加载中/成功/失败"的状态管理从组件内部提升到了组件外部。最佳实践:
<ErrorBoundary>包裹<Suspense>,分别处理错误和加载态。Suspense 的底层原理是"throw Promise "------子组件在数据未就绪时抛出 Promise,Suspense 捕获后显示 fallback,Promise resolve 后重新渲染。React 19 的use()让 Suspense 的使用更加直观。
26. React Server Components(RSC)原理
什么是 Server Components?
Server Components 是只在服务器上运行的 React 组件 ------它们在服务端渲染,生成的是一种特殊的序列化格式(RSC Payload),而不是 HTML。客户端接收后直接渲染,不需要下载组件的 JS 代码。
| 特性 | Server Components | Client Components | 传统 SSR |
|---|---|---|---|
| 运行环境 | 仅服务器 | 服务器 + 客户端 | 服务器渲染,客户端 Hydrate |
| JS 发送到客户端 | ❌ 不发送 | ✅ 需要发送 | ✅ 需要发送 |
| 访问后端资源 | ✅ 直接访问 DB/文件 | ❌ 需要 API | ❌ 需要 API |
| useState/useEffect | ❌ 不支持 | ✅ 支持 | ✅ 支持 |
| 事件处理(onClick) | ❌ 不支持 | ✅ 支持 | ✅ 支持 |
| 包体积影响 | ✅ 零影响 | 计入 Bundle | 计入 Bundle |
arduino
// ========== RSC 渲染流程 ==========
// 1. 请求到达服务器
// 2. Server Components 在服务器执行(可以直接读 DB、调内部 API)
// 3. 生成 RSC Payload(序列化的组件树 + 数据)
// 4. 流式传输到客户端
// 5. 客户端 React Runtime 接收 RSC Payload
// 6. 将 Server Components 渲染为 DOM
// 7. Client Components 在客户端 Hydrate(绑定事件等)
// 与传统 SSR 的区别:
// SSR:服务端生成 HTML → 客户端下载 JS → Hydrate(全量)
// RSC:服务端生成 RSC Payload → 只下载 Client Components 的 JS → 部分 Hydrate
"use client" 和 "use server"
jsx
// ========== 1. Server Component(默认,不需要标记) ==========
// app/page.tsx(Next.js App Router 中默认是 Server Component)
// ✅ 可以直接在组件中访问数据库
import { db } from '@/lib/db'
async function UserList() {
// ✅ 直接查数据库(不需要 API 接口!)
const users = await db.user.findMany()
// ✅ 可以使用大型库(不会打包到客户端)
import dayjs from 'dayjs' // 不计入客户端 bundle
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} - {dayjs(user.createdAt).format('YYYY-MM-DD')}
</li>
))}
</ul>
)
}
// ========== 2. Client Component(用 "use client" 标记) ==========
// components/LikeButton.tsx
'use client' // ⚠️ 必须在文件顶部(不是组件顶部)
import { useState } from 'react'
export function LikeButton({ initialCount }) {
// ✅ Client Component 可以使用 Hooks
const [count, setCount] = useState(initialCount)
// ✅ 可以绑定事件
return (
<button onClick={() => setCount(c => c + 1)}>
❤️ {count}
</button>
)
}
// ========== 3. Server Component 中使用 Client Component ==========
// app/posts/[id]/page.tsx(Server Component)
import { db } from '@/lib/db'
import { LikeButton } from '@/components/LikeButton' // Client Component
async function PostPage({ params }) {
const post = await db.post.findUnique({ where: { id: params.id } })
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* ✅ Server Component 可以渲染 Client Component */}
{/* Server 侧将 initialCount 作为 props 传递 */}
<LikeButton initialCount={post.likes} />
</article>
)
}
Server Actions("use server")
jsx
// ========== 4. Server Actions:从客户端调用服务端函数 ==========
// 方式1:独立文件
// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
// ✅ 在服务端执行:可以直接操作数据库
await db.post.create({ data: { title, content } })
// 重新验证缓存,触发页面更新
revalidatePath('/posts')
}
// 方式2:内联在 Server Component 中
async function PostForm() {
async function handleSubmit(formData: FormData) {
'use server' // 标记为 Server Action
await db.post.create({
data: { title: formData.get('title') }
})
revalidatePath('/posts')
}
return (
<form action={handleSubmit}>
<input name="title" />
<button type="submit">发布</button>
</form>
)
}
// 在 Client Component 中使用 Server Action
'use client'
import { createPost } from './actions'
import { useActionState } from 'react'
function CreatePostForm() {
const [state, formAction, isPending] = useActionState(createPost, null)
return (
<form action={formAction}>
<input name="title" />
<button disabled={isPending}>
{isPending ? '发布中...' : '发布'}
</button>
</form>
)
}
Server / Client 组件选型
arduino
// ========== 组件选型速查表 ==========
| 需求 | Server Component | Client Component |
|-------------------------------|:---------------:|:---------------:|
| 获取数据(DB/API) | ✅ | ❌(需要 useEffect) |
| 访问后端资源(文件系统等) | ✅ | ❌ |
| 使用大型依赖(不影响 Bundle) | ✅ | ❌ |
| 使用 Hooks(useState 等) | ❌ | ✅ |
| 绑定事件(onClick 等) | ❌ | ✅ |
| 使用浏览器 API(localStorage) | ❌ | ✅ |
| 使用 Context | ❌ | ✅ |
// ========== 设计原则 ==========
// 1. 默认使用 Server Component(零 JS 发送到客户端)
// 2. 只在需要交互/Hooks/浏览器 API 时才用 Client Component
// 3. 将 Client Component 推到组件树的叶子节点(最小化客户端 JS)
// 4. Server Component 可以 import Client Component,反之不行
// 5. 可以将 Server Component 作为 children 传给 Client Component
// 模式:
// <ServerLayout> ← Server(不发送 JS)
// <Header /> ← Server
// <SearchBar /> ← Client(有输入交互)
// <ProductList> ← Server(直接查 DB)
// <AddToCartButton /> ← Client(有点击交互)
// </ProductList>
// </ServerLayout>
💡 面试加分点: RSC 的核心价值是"零 Bundle 的服务端组件 "------Server Components 不会发送任何 JS 到客户端,极大减少 Bundle 体积。与传统 SSR 的本质区别:SSR 是"先在服务端渲染 HTML,再在客户端全量 Hydrate",RSC 是"服务端组件只运行在服务端,客户端只 Hydrate Client Components"。设计原则是"默认 Server,按需 Client "------只有需要交互(事件/Hooks/浏览器 API)的组件才标记为 Client Component。Server Actions(
"use server")则解决了客户端直接调用服务端逻辑的问题,相当于自动生成了 API 接口。