React 面试题大全
本文档《React 高频面试题》(26 题)基础上,补充了大量基础面试题,供大家参考。
第一部分:React 基础概念
1. React 的核心概念是什么?
核心思想:UI = f(state)
| 核心概念 | 说明 |
|---|---|
| JSX | JavaScript 语法扩展,编译为 React.createElement() 调用 |
| 组件 | UI 的基本构建单元,分函数组件和类组件 |
| 单向数据流 | 数据从父组件通过 props 向下流动 |
| 虚拟 DOM | 内存中的轻量 DOM 表示,通过 diff 算法最小化真实 DOM 操作 |
| 声明式 | 描述 UI 应该是什么样子,而不是如何操作 DOM |
面试加分点: 函数组件每次渲染都捕获当前的 props 和 state(闭包特性),而类组件通过
this读取的总是最新值——这就是著名的 "Capture Value" 特性。
2. 什么是 SPA?React 如何实现 SPA?
SPA(Single Page Application)即单页面应用,整个应用只有一个 HTML 页面,通过 JavaScript 动态更新 DOM 实现页面切换,无需向服务器请求新页面。
React 实现 SPA 的方式:
- 使用 React Router 等库管理 URL 和视图切换
- 监听
hashchange事件(Hash 路由)或使用 History API(Browser 路由) - 所有路由对应的组件在客户端渲染,不重新加载页面
优缺点:
| 优点 | 缺点 |
|---|---|
| 用户体验流畅,无白屏刷新 | 首屏加载慢(需下载整个 JS bundle) |
| 前后端分离,开发效率高 | SEO 不友好(搜索引擎爬虫难以抓取) |
| 减少服务器压力 | 需要处理浏览器兼容问题 |
3. React 是单向数据流还是双向数据流?为什么?
React 采用单向数据流。 数据从父组件通过 props 传递给子组件,子组件不能直接修改父组件的数据。
为什么不用双向数据流(如 Angular 的 ng-model)?
- 可预测性:数据变化路径清晰,便于调试和追踪
- 性能:避免双向绑定带来的额外监听和更新开销
- 简单性:不需要维护复杂的依赖关系图
如果需要子组件修改父组件数据怎么办?
- 通过回调函数:父组件传递
onUpdate回调给子组件,子组件调用回调触发更新 - 使用状态管理库(Redux、Zustand 等)
4. 函数组件和类组件的区别?
| 维度 | 函数组件 | 类组件 |
|---|---|---|
| 写法 | 纯函数,返回 JSX | ES6 类,继承 Component |
| this | 无 this(或指向 undefined) | 有 this,指向组件实例 |
| 状态管理 | useState Hook | this.state + setState |
| 生命周期 | useEffect 模拟 | componentDidMount/Update/Unmount 等 |
| 性能 | 略优(无实例开销) | 略差(需要创建实例) |
| 逻辑复用 | 自定义 Hook | HOC / Render Props |
| 错误捕获 | 无法作为 Error Boundary | 可实现 componentDidCatch |
| 异步问题 | 存在闭包陷阱 | 无闭包陷阱(this 始终指向实例) |
面试加分点: React 官方已明确推荐使用函数组件 + Hooks,类组件将逐步边缘化。React 19 中 ref 作为普通 prop,forwardRef 也将废弃,这些都标志着函数组件的全面胜利。
5. JSX 是什么?它和 HTML 有什么区别?
JSX(JavaScript Syntax Extension)是 JavaScript 的语法扩展,允许在 JS 中书写类似 HTML 的结构,最终被 Babel 编译为 React.createElement() 调用。
JSX vs HTML 的区别:
// 1. class → className(因为 class 是 JS 保留字)
<div className="container">
// 2. for → htmlFor
<label htmlFor="name">Name</label>
// 3. 自闭合标签必须写 />
// HTML 是宽松语法,浏览器会自动补全标签;
// JSX 本质是 **JavaScript 的语法糖**,会被 Babel 编译成 `React.createElement`,遵循 XML 严格语法规则。
// XML 规定:没有子节点的空标签,必须以 `/>` 结尾,不能只写 `>`
// 否则会报错
<input type="text" />
<br />
// 4. 可以嵌入 JS 表达式(用 {} 包裹)
<p>{count > 0 ? '正数' : '非正数'}</p>
<p>{items.map(item => <li key={item.id}>{item.name}</li>)}</li>
// 5. style 接受对象而非字符串
<div style={{ color: 'red', fontSize: '14px' }} />
// 6. 事件名用驼峰命名
<button onClick={handleClick} />
<input onChange={handleChange} />
编译过程:
// JSX 源码
const element = <div className="app"><h1>Hello</h1></div>
// 编译后
const element = React.createElement(
'div',
{ className: 'app' },
React.createElement('h1', null, 'Hello')
)
6. React 的声明式和命令式编程有什么区别?
| 维度 | 声明式(React) | 命令式(jQuery) |
|---|---|---|
| 编程思路 | 描述 UI "应该是什么" | 描述"如何一步步操作 DOM" |
| 关注点 | 结果(What) | 过程(How) |
| DOM 操作 | React 自动处理 | 开发者手动操作 |
| 代码可读性 | 高,直观描述 UI 结构 | 低,大量 DOM 操作代码 |
| 示例 | <button disabled={!isReady}> | $('#btn').prop('disabled', !isReady) |
举例对比:
// 命令式:告诉 React 如何操作 DOM
const input = document.getElementById('input')
input.value = 'hello'
input.dispatchEvent(new Event('input'))
updateButtonState(input.value)
// 声明式:只描述 UI 应该长什么样
<input value={text} onChange={e => setText(e.target.value)} />
<button disabled={!text}>提交</button>
7. 什么是 React 元素(Element)?它和 DOM 节点的关系?
React 元素是一个普通的 JavaScript 对象,描述了应该渲染什么样的 UI:
// React 元素的结构
const element = {
type: 'div', // 标签名或组件函数
props: { // 属性
className: 'container',
children: [...] // 子元素
}
}
关系链:
JSX → React.createElement() → React Element(虚拟 DOM 节点)→ ReactDOM.render() → 真实 DOM
React 元素是虚拟 DOM 的最小单位,一个真实 DOM 节点对应一个 React 元素(但不是一一对应,比如 Fragment 不产生真实 DOM)。
8. React 组件的通信方式有哪些?
| 方式 | 适用场景 | 复杂度 |
|---|---|---|
| props | 父子组件通信 | ⭐ |
| state + callback | 子组件通知父组件 | ⭐ |
| Context | 跨多层组件共享数据(如主题、语言) | ⭐⭐ |
| 自定义 Hook | 逻辑复用 + 状态共享 | ⭐⭐ |
| ref | 父组件访问子组件 DOM 或方法 | ⭐⭐ |
| 状态管理库(Redux/Zustand) | 全局状态、复杂数据流 | ⭐⭐⭐ |
| 事件总线/mitt | 任意组件间通信(不推荐,难以追踪) | ⭐⭐ |
| URL 参数 | 路由传参 | ⭐ |
选择原则: 优先 props/state,跨层级用 Context,全局状态用状态管理库。
9. React 中为什么不能在循环、条件语句或嵌套函数中使用 Hooks?
这源于 Hooks 的实现原理——链表。React 内部按 Hooks 的调用顺序将它们存储在一个链表中。
第一次渲染:useState → useEffect → useState → useEffect
↓ ↓ ↓ ↓
链表: [node1] → [node2] → [node3] → [node4]
第二次渲染:useState → useEffect → useState → useEffect
↓ ↓ ↓ ↓
链表: [node1] → [node2] → [node3] → [node4]
如果在条件语句中使用 Hooks:
function App({ isLoggedIn }) {
const [user, setUser] = useState(null) // 第 1 个 Hook
if (isLoggedIn) {
const [name, setName] = useState('') // 第 2 个 Hook(条件内)
}
useEffect(() => { /* ... */ }, []) // 第 3 个 Hook
}
当 isLoggedIn 从 true 变为 false 时,Hooks 数量从 3 个变成 2 个,链表结构错乱,导致状态混乱。
解决方案: 将条件逻辑提取到单独的组件中,确保 Hooks 调用顺序始终一致。
10. React 的事件系统是如何工作的?
React 实现了一套合成事件系统(SyntheticEvent),统一了各浏览器的 DOM 事件差异。
工作原理:
- React 在组件树根部(React 17+ 挂在 root 节点,之前挂在 document)注册原生事件监听器
- 所有子组件的事件都会冒泡到这个根节点
- 根节点根据事件类型和目标,分发到对应的 React 组件
真实 DOM 事件流:
target → currentTarget(冒泡阶段)→ window
React 合成事件流:
target → parent → ... → root(React 容器)
面试加分点:
- React 17 把事件委托从
document改到了ReactDOM.render()的根节点,好处是支持微前端多个 React 实例共存 e.stopPropagation()只能阻止 React 合成事件的冒泡,无法阻止原生事件- 合成事件和原生事件的执行顺序:先原生事件,后合成事件(因为原生事件先冒泡到 document/window)
11. React 组件的 props 是只读的吗?
是的,props 是只读的。 组件不应该修改自己的 props。
// ❌ 错误:直接修改 props
function UserCard({ user }) {
user.name = 'New Name' // 不要这样做!
return <div>{user.name}</div>
}
// ✅ 正确:通过回调让父组件修改
function UserCard({ user, onNameChange }) {
return (
<div>
{user.name}
<button onClick={() => onNameChange(user.id, 'New Name')}>
修改名字
</button>
</div>
)
}
原因: 单向数据流保证了数据变化的可追踪性。如果允许子组件随意修改 props,数据流向变得不可预测,调试困难。
12. 什么是 React 的 Fiber 架构?解决了什么问题?
Fiber 是 React 16 引入的新协调引擎,解决了旧版递归渲染的两个问题:
- 不可中断:旧版递归一旦开始就不能暂停,大型组件树会导致主线程长时间阻塞
- 优先级无法区分:所有更新同等对待,用户输入等高优先级更新可能被低优先级更新阻塞
Fiber 的核心设计:
- 将递归的调用栈转换为链表结构的 Fiber 节点
- 每个 Fiber 节点是一个"工作单元"(Unit of Work)
- React 可以在执行过程中暂停、恢复、丢弃工作
- 实现了双缓冲树(current 树和 workInProgress 树)
Stack(旧) Fiber(新)
┌─────────┐ ┌─────────┐
│ App │ ← 递归调用栈 │ App │ ← Fiber 链表
│ ├─ A │ 不可中断 │ ├─ A │ 可中断
│ ├─ B │ │ ├─ B │
│ └─ C │ │ └─ C │
└─────────┘ └─────────┘
两个阶段:
- Render 阶段(Reconciliation):构建新的 Fiber 树,可中断
- Commit 阶段:将变更应用到真实 DOM,不可中断
13. React 中 useState 的初始值可以是函数吗?
可以,而且是惰性初始化(Lazy Initialization)的最佳实践。
// ❌ 每次渲染都会执行 JSON.parse
const [data, setData] = useState(JSON.parse(localStorage.getItem('key')))
// ✅ 只有第一次渲染时执行,后续渲染跳过
const [data, setData] = useState(() => {
console.log('只在首次渲染时执行')
return JSON.parse(localStorage.getItem('key'))
})
函数式初始值的意义:
- 避免昂贵的计算在每次渲染时重复执行
- 避免读取 localStorage/sessionStorage 等 I/O 操作的重复开销
- 保证初始值的一致性(不会因为渲染次数不同而变化)
14. React 中 setState 是同步还是异步的?
取决于调用场景:
| 场景 | 是否异步 | 原因 |
|---|---|---|
| 事件处理函数中 | 异步(批量) | React 会合并多次 setState 为一次更新 |
| setTimeout/Promise 中 | 同步 | 脱离了 React 的事件系统管控 |
| 原生 DOM 事件中 | 同步 | 同上 |
| React 18 createRoot 下 | 全部异步(自动批处理) | Automatic Batching |
function Counter() {
const [count, setCount] = useState(0)
// 异步(批量更新)
const handleClick = () => {
setCount(count + 1) // 不会立即更新
setCount(count + 1) // 两次合并为一次,最终 count += 1
}
// 同步
const handleTimer = () => {
setTimeout(() => {
setCount(count + 1) // 立即更新
}, 1000)
}
}
React 18 的变化: 使用 createRoot 后,所有场景都自动批量更新,包括 setTimeout、Promise、原生事件等。
面试加分点: 如果需要强制同步更新,使用
flushSync(() => setCount(count + 1))。
15. React 组件渲染的完整流程是什么?
1. 触发更新
├── 初始渲染:ReactDOM.render(<App />, container)
└── 更新渲染:setState / useState 的 dispatch / Context 变化 / props 变化
2. Reconciliation(协调)—— Render 阶段,可中断
├── 构建新的 Fiber 树(workInProgress 树)
├── 新旧 Fiber 树 Diff 比较
└── 标记副作用(Placement / Update / Deletion)
3. Commit —— 提交阶段,不可中断
├── before mutation:获取快照(getSnapshotBeforeUpdate)
├── mutation:真正修改 DOM(增删改)
└── layout:DOM 已修改,可安全读写 DOM
├── useLayoutEffect 执行
├── componentDidUpdate / componentDidMount 执行
└── getSnapshotBeforeUpdate 回调
4. 收尾
├── 切换 Fiber 树指针(current ↔ workInProgress)
└── 执行 effect(useEffect / componentDidUpdate)
16. React 中为什么要使用不可变数据(Immutable Data)?
核心原因:React 通过引用来判断数据是否变化。 如果直接修改数据,React 无法检测到变化,不会触发重新渲染。
// ❌ 直接修改数组,React 检测不到变化
const [list, setList] = useState([1, 2, 3])
list.push(4)
setList(list) // 引用没变,可能不触发重渲染
// ✅ 创建新数组,引用改变,React 检测到变化
setList([...list, 4])
不可变更新的常见模式:
// 数组
const newArr = [...oldArr, newItem] // 追加
const newArr = oldArr.filter(id => id !== 3) // 删除
const newArr = oldArr.map(item => item.id === 3 ? {...item, name: 'new'} : item) // 更新
// 对象
const newObj = {...oldObj, newKey: value} // 更新/新增
const newObj = {...oldObj}
delete newObj.key // 删除
面试加分点: 对于深层嵌套结构,可以使用 Immer 库或 Redux Toolkit 的
produce来简化不可变更新。
17. React 中 children 是什么?如何操作 children?
children 是组件的一个特殊 prop,代表组件标签之间的内容。
function Card({ children }) {
return (
<div className="card">
<div className="card-header">标题</div>
<div className="card-body">{children}</div>
</div>
)
}
// 使用
<Card>
<p>这是 children 的内容</p>
</Card>
children 的类型:
typeof children
// 'string' → 纯文本
// 'number' → 数字
// 'object' → React 元素或元素数组
// 'boolean' → true/false
// 'function' → render props
// undefined → 无内容
常用操作:
// 判断是否有内容
{children && <div>{children}</div>}
// 遍历子元素
{React.Children.map(children, child => (
<div key={child.key}>{child}</div>
))}
// 克隆子元素并添加 props
{React.Children.map(children, child =>
React.cloneElement(child, { additionalProp: true })
)}
// 验证 children 类型
PropTypes.nodeTypes.childrenToContent
18. React 中 Fragment 的作用和使用方式?
Fragment(片段)允许在不引入额外 DOM 元素的情况下包裹多个子元素。
两种写法:
// 简写形式(空标签)
<>
<div>A</div>
<div>B</div>
</>
// 显式形式(可添加 key 等属性)
<React.Fragment key="unique-key">
<div>A</div>
<div>B</div>
</React.Fragment>
作用:
- 避免不必要的 DOM 嵌套(减少对 CSS 布局的影响)
- 解决列表渲染中缺少单一父元素的警告
- 提高性能(少一层 DOM 节点)
注意事项:
- Fragment 不是真正的 DOM 元素,CSS 选择器无法选中它
- 短语法
<>不支持添加属性(如 key),需要用<React.Fragment>形式 - 组件返回 Fragment 时,不能使用 ref(除非用 forwardRef)
19. React 中高阶组件(HOC)是什么?举一个例子。
高阶组件(Higher-Order Component)是一个接收组件作为参数并返回新组件的函数。
// 一个日志 HOC
function withLogger(WrappedComponent) {
return function LoggedComponent(props) {
useEffect(() => {
console.log(`${WrappedComponent.name} 组件挂载了`)
}, [])
return <WrappedComponent {...props} />
}
}
// 使用
function UserProfile({ name }) {
return <div>{name}</div>
}
const UserProfileWithLogger = withLogger(UserProfile)
常见 HOC 应用场景:
- 权限控制(withAuth)
- 日志埋点(withLogger)
- 注入全局配置(withTheme)
- 连接 Redux(connect)
HOC 的问题:
- 嵌套地狱(HOC 套 HOC 套 HOC)
- props 命名冲突(多个 HOC 都想注入同名 prop)
- 可读性差,调试困难
现代替代方案: 自定义 Hook 几乎完全取代了 HOC。
20. React 中 render props 是什么?
Render Props 是一种在组件之间共享代码的技术,通过传递一个函数作为 prop 来实现。
// MouseTracker 组件:追踪鼠标位置
class MouseTracker extends Component {
state = { x: 0, y: 0 }
handleMouseMove = (e) => {
this.setState({ x: e.clientX, y: e.clientY })
}
render() {
return (
<div onMouseMove={this.handleMouseMove}>
{/* 将鼠标位置作为参数传给 render 函数 */}
{this.props.children({ x: this.state.x, y: this.state.y })}
</div>
)
}
}
// 使用
<MouseTracker>
{({ x, y }) => (
<div>鼠标位置:x={x}, y={y}</div>
)}
</MouseTracker>
与 HOC 对比:
- HOC:逻辑在外部,通过增强组件实现复用
- Render Props:逻辑在外部,通过回调函数实现复用
- 两者都可以被自定义 Hook 替代
第二部分:组件与生命周期
21. React 组件的生命周期是怎样的?
类组件生命周期
mounted
↑
constructor | componentWillUnmount
| | |
didMount ← update ← didUpdate
|
willUpdate
| 阶段 | 生命周期方法 | 作用 |
|---|---|---|
| 挂载 | constructor | 初始化 state、绑定方法 |
| 挂载 | componentDidMount | DOM 已挂载,发请求、订阅事件 |
| 更新 | componentDidUpdate | DOM 已更新,操作更新后的 DOM |
| 卸载 | componentWillUnmount | 清理定时器、取消订阅 |
函数组件对应的 Hook
| 类组件 | 函数组件 |
|---|---|
| constructor + componentDidMount | useEffect(() => {}, []) |
| componentDidUpdate | useEffect(() => {}, [deps]) |
| componentWillUnmount | useEffect(() => { return cleanup }, []) |
| shouldComponentUpdate | React.memo |
| getDerivedStateFromProps | 渲染时直接从 props 计算(无需 Hook) |
22. useEffect 的依赖数组为空、省略和不传有什么区别?
function App() {
const [count, setCount] = useState(0)
// 1. 空依赖数组 []:只在挂载和卸载时执行
useEffect(() => {
console.log('只在挂载时执行一次')
return () => console.log('卸载时清理')
}, [])
// 2. 省略依赖数组:每次渲染后都执行(危险!)
useEffect(() => {
console.log('每次渲染后都执行', count)
})
// 3. 不传依赖数组(等同于省略)
useEffect(() => {
console.log('也是每次渲染后都执行', count)
})
// 4. 有依赖:依赖变化时执行
useEffect(() => {
console.log('count 变化时执行', count)
}, [count])
}
记忆口诀:
[]→ 只跑一次(挂载)- 不传 → 每次都跑
[dep]→ dep 变了才跑
23. useEffect 和 useLayoutEffect 的区别?
| 维度 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | DOM 修改后,异步执行 | DOM 修改后,浏览器绘制前同步执行 |
| 是否阻塞渲染 | 不阻塞 | 阻塞(直到 effect 执行完毕才绘制) |
| 典型用途 | 数据获取、日志、埋点 | 读取 DOM 尺寸、避免闪烁 |
| SSR 兼容 | 支持 | 不支持(会警告) |
function Tooltip() {
const [position, setPosition] = useState({ x: 0, y: 0 })
// useLayoutEffect:在浏览器绘制前计算位置,避免闪烁
useLayoutEffect(() => {
const rect = tooltipRef.current.getBoundingClientRect()
setPosition({ x: rect.left, y: rect.top })
}, [])
return <div ref={tooltipRef} style={position}>Tooltip</div>
}
为什么需要 useLayoutEffect? 如果用 useEffect,用户可能会看到元素在错误位置的瞬间闪烁(flash of incorrect content)。
24. useMemo 和 useCallback 的区别和使用场景?
useMemo 缓存的是值(计算结果),useCallback 缓存的是函数。
function ExpensiveComponent({ data, onItemClick }) {
// useMemo:缓存计算结果,只有 data 变化时才重新计算
const sortedData = useMemo(() => {
console.log('排序计算中...')
return data.sort((a, b) => a.value - b.value)
}, [data])
// useCallback:缓存函数引用,只有 onItemClick 变化时才创建新函数
const handleClick = useCallback((id) => {
onItemClick(id)
}, [onItemClick])
return (
<div>
{sortedData.map(item => (
<Item key={item.id} onClick={() => handleClick(item.id)} />
))}
</div>
)
}
什么时候用?
- 传给
React.memo包裹的子组件的 props 需要稳定引用时 - 作为其他 Hook(如 useEffect)的依赖项时
- 计算密集型操作需要缓存结果时
什么时候不要用?
- 简单的计算(如
count * 2)—— useMemo 本身也有开销 - 没有传给子组件的普通变量
- 频繁变化的依赖——缓存反而增加开销
面试陷阱:
-
useCallback(fn, [])会创建一个永远不会更新的函数,如果函数内部使用了外部变量,会导致闭包陷阱。 -
这个函数内部捕获的是初次渲染的变量快照,后续 state/props 更新了,函数里拿到的还是旧值,就是闭包陷阱。
25. useRef 有哪些使用场景?
function MyComponent() {
// 场景 1:保存上一次的值(逃逸闭包)
const prevValueRef = useRef()
const [value, setValue] = useState(0)
useEffect(() => {
prevValueRef.current = value // 记录上一次的 value
}, [value])
// 场景 2:引用 DOM 元素
const inputRef = useRef<HTMLInputElement>(null)
// 场景 3:保存不触发重渲染的值(如定时器 ID、请求取消函数)
const timerRef = useRef<number | null>(null)
const abortControllerRef = useRef<AbortController | null>(null)
// 场景 4:模拟类组件的实例变量
const instanceVarRef = useRef(0)
return (
<div>
<input ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>聚焦</button>
</div>
)
}
核心特性: useRef 返回一个可变容器,修改 .current 不会触发重渲染(这是它与 useState 最大的区别)。
26. React.memo 的作用和原理是什么?
React.memo 是一个高阶组件,用于对函数组件进行浅比较(shallow comparison),只有当 props 发生变化时才重新渲染。
// 不加 memo:父组件每次渲染,Child 都会重新渲染
function Child({ name }) {
console.log('Child 渲染了')
return <div>{name}</div>
}
// 加 memo:只有 name 变化时才重新渲染
const MemoizedChild = React.memo(Child)
function Parent() {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(c => c + 1)}>count: {count}</button>
<MemoizedChild name="Alice" /> {/* name 不变,Child 不渲染 */}
</div>
)
}
原理: React.memo 在 commit 阶段对比新旧 props,如果所有 props 都相等(浅比较),就跳过该组件的渲染。
自定义比较函数:
const MemoizedChild = React.memo(Child, (prevProps, nextProps) => {
// 返回 true 表示 props 相等,跳过渲染
return prevProps.name === nextProps.name
})
注意事项:
- 只对直接 props 做浅比较,嵌套对象/数组需要展开比较
- 如果组件本身渲染开销很小,memo 的收益不明显
- 过度使用 memo 会增加代码复杂度和内存占用
27. React 中 key 的作用是什么?为什么不能用 index?
key 是 React 识别组件身份的标识。 在 diff 过程中,React 通过 key 判断哪些元素是同一个元素(可以复用),哪些需要删除或新建。
三大规则:
- 稳定——同一数据生命周期内 key 不变
- 唯一——同层兄弟节点间唯一
- 可预测——不要用 index 做有状态列表的 key
为什么不能用 index 做 key?
// 假设列表初始为 [A, B, C]
<List items={['A', 'B', 'C']} />
// 删除第一项后变为 [B, C]
<List items={['B', 'C']} />
如果用 index 做 key:
- 原来 key=0 对应 A,现在 key=0 对应 B
- React 认为 key=0 的元素从 A 变成了 B,触发不必要的更新
- 如果子组件有状态(如输入框),状态会发生错位
正确的做法: 用数据的唯一标识(如 id)做 key。
28. React 组件何时应该拆分?
拆分原则: 单一职责——一个组件只做一件事。
具体信号:
- 组件过大:超过 200-300 行,难以阅读和维护
- 逻辑过重:包含多种不相关的业务逻辑
- 重复渲染:某部分 UI 频繁更新,但父组件其余部分不需要更新
- 复用需求:同一 UI 在多处使用
- 测试困难:组件功能太多,难以编写单元测试
拆分方向:
- UI 拆分:提取子组件(如 Header、Sidebar、Content)
- 逻辑拆分:提取自定义 Hook(如 useAuth、useFetch)
- 数据拆分:将全局状态移到状态管理库
29. React 中受控组件和非受控组件的区别?
| 维度 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据驱动 | React state | DOM 自身 |
| 值 | value={state} | 无 value 绑定 |
| 修改方式 | onChange 更新 state | 用户直接修改 DOM |
| 实时验证 | 支持 | 不方便 |
| 性能 | 每次输入都重渲染 | 性能好 |
| 典型场景 | 表单验证、密码强度提示 | 文件上传、富文本编辑器 |
// 受控组件
function ControlledInput() {
const [text, setText] = useState('')
return <input value={text} onChange={e => setText(e.target.value)} />
}
// 非受控组件
function UncontrolledInput() {
const ref = useRef(null)
const handleSubmit = () => {
console.log(ref.current.value) // 提交时才读取
}
return <input ref={ref} defaultValue="" />
}
生产环境推荐: 使用 React Hook Form 库,结合了受控和非受控的优点。
第三部分:状态管理
30. React 有哪些状态管理方案?如何选择?
| 方案 | 适用场景 | 复杂度 | 特点 |
|---|---|---|---|
| useState + Context | 小型应用、低频更新 | ⭐ | 内置方案,无需额外依赖 |
| Redux Toolkit | 大型团队、复杂数据流 | ⭐⭐⭐ | 严格的单向数据流,DevTools 强大 |
| Zustand | 中小型应用、快速开发 | ⭐⭐ | 极简 API,无 boilerplate |
| Jotai | 原子化状态、按需渲染 | ⭐⭐ | 原子化模型,细粒度更新 |
| MobX | 复杂响应式数据 | ⭐⭐⭐ | 基于 Observable,自动追踪依赖 |
| TanStack Query | 服务端状态(API 数据) | ⭐⭐ | 缓存、去重、后台刷新、乐观更新 |
选型决策树:
数据来自哪里?
├── 服务端(API)→ TanStack Query / SWR
└── 客户端
├── 只在组件内部 → useState
├── 跨组件共享(少量)→ Context
├── 跨组件共享(大量/复杂)→ Zustand(推荐)/ Redux Toolkit
└── 需要持久化 → Zustand + persist / localStorage
31. useState 和 useReducer 的区别?什么时候用 useReducer?
| 维度 | useState | useReducer |
|---|---|---|
| 适用场景 | 简单状态(单个值) | 复杂状态(多个子值相互关联) |
| 更新方式 | setState(newValue) | dispatch({ type, payload }) |
| 状态逻辑 | 更新逻辑在组件内 | 更新逻辑集中在 reducer 函数中 |
| 调试 | 只能看最终值 | 可追溯每次 dispatch |
| 性能 | 每次更新都可能触发重渲染 | 可根据 action 决定是否 bailout |
什么时候用 useReducer?
- 状态逻辑复杂(多个子状态相互依赖)
- 需要根据前一个状态计算下一个状态
- 需要在多个组件间共享相同的更新逻辑
- 需要频繁测试状态更新逻辑
// 复杂表单场景
const initialState = { name: '', age: 0, email: '' }
function formReducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return { ...state, [action.field]: action.value }
case 'RESET':
return initialState
default:
return state
}
}
function Form() {
const [form, dispatch] = useReducer(formReducer, initialState)
// ...
}
32. Redux 的核心概念和工作原理?
三个核心概念:
- Store:单一数据源,整个应用的状态存在一个对象中
- Action:描述"发生了什么"的普通对象,必须有
type字段 - Reducer:纯函数,接收当前 state 和 action,返回新的 state
工作流程:
User Action → dispatch(action) → Store.dispatch() → Reducer(state, action) → New State → View 更新
// 1. 定义 Action Type
const ADD_TODO = 'ADD_TODO'
// 2. 定义 Action Creator
const addTodo = (text) => ({ type: ADD_TODO, payload: text })
// 3. 定义 Reducer(纯函数)
function todosReducer(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, action.payload]
default:
return state
}
}
// 4. 创建 Store
const store = createStore(todosReducer)
// 5. 派发 Action
store.dispatch(addTodo('Learn Redux'))
Redux Toolkit(现代写法):
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
// 异步 Action
export const fetchTodos = createAsyncThunk('todos/fetch', async () => {
const response = await api.getTodos()
return response.data
})
// Slice(自动处理 immutable 更新)
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push(action.payload) // Immer 允许"直接修改"
}
},
extraReducers: (builder) => {
builder.addCase(fetchTodos.fulfilled, (state, action) => {
state.push(...action.payload)
})
}
})
33. Context 的性能问题及优化方案?
问题: Context 的 value 变化时,所有消费者组件都会重新渲染,即使它们只消费了未变化的部分。
// ❌ 问题:theme 变化时,App 中所有内容都会重渲染
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<App /> {/* 整棵树重渲染 */}
</ThemeContext.Provider>
优化方案:
// 方案 1:useMemo 缓存 value
const value = useMemo(() => ({ theme, toggleTheme }), [theme, toggleTheme])
<ThemeContext.Provider value={value}>
// 方案 2:拆分 Context(按领域拆分)
<ThemeContext.Provider value={themeValue}>
<LanguageContext.Provider value={langValue}>
<App />
</LanguageContext.Provider>
</ThemeContext.Provider>
// 方案 3:将消费组件独立出来,用 React.memo 包裹
const ThemeDisplay = React.memo(function ThemeDisplay({ theme }) {
return <div>{theme}</div>
})
// 方案 4:使用 use-context-selector 只订阅需要的字段
const theme = useContextSelector(ThemeContext, ctx => ctx.theme)
34. Zustand 的核心原理和用法?
Zustand 是一个轻量级的状态管理库,核心是一个 create 函数返回一个 Hook。
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
// 基础用法
const useStore = create((set, get) => ({
// 状态
count: 0,
user: null,
// Actions(直接修改状态,无需 dispatch)
increment: () => set((state) => ({ count: state.count + 1 })),
setUser: (user) => set({ user }),
decrement: () => set((state) => ({ count: state.count - 1 })),
}))
// 组件中使用
function Counter() {
// 选择性订阅(只订阅 count,count 不变则不重渲染)
const count = useStore((state) => state.count)
const increment = useStore((state) => state.increment)
return <button onClick={increment}>{count}</button>
}
// 持久化中间件
const usePersistedStore = create(
persist(
(set) => ({
token: null,
setToken: (token) => set({ token }),
}),
{ name: 'storage-key' } // localStorage 的 key
)
)
为什么 Zustand 比 Redux 简单?
- 没有 Action/Reducer 的概念,直接在 store 中定义 actions
- 不需要 Provider 包裹
- 内置选择器机制,自动优化渲染
- 支持中间件(persist、devtools、immer 等)
第四部分:Hooks 深入
35. 列举你常用的自定义 Hook 及其实现。
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
}
useLocalStorage(本地存储同步)
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setValue = (value) => {
setStoredValue(value)
localStorage.setItem(key, JSON.stringify(value))
}
return [storedValue, setValue]
}
usePrevious(获取上一次的值)
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
useMediaQuery(响应式检测)
function useMediaQuery(query) {
const [matches, setMatches] = useState(() => {
return window.matchMedia(query).matches
})
useEffect(() => {
const mql = window.matchMedia(query)
const handler = (e) => setMatches(e.matches)
mql.addEventListener('change', handler)
return () => mql.removeEventListener('change', handler)
}, [query])
return matches
}
useFetch(数据请求)
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [url])
return { data, loading, error }
}
useToggle(布尔切换)
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue)
const toggle = () => setValue(v => !v)
return [value, toggle]
}
36. React Hooks 的规则(Rules of Hooks)是什么?
- 只在顶层调用 Hook——不要在循环、条件语句、嵌套函数或 return 之后使用 Hook
- 只在 React 函数中调用 Hook——不要在普通 JS 函数中使用
- 自定义 Hook 必须以
use开头——方便 ESLint 插件识别和检查
为什么有这些规则? React 依赖 Hook 的调用顺序来建立内部状态链表。如果顺序不一致,状态会错乱。
ESLint 插件: eslint-plugin-react-hooks 可以自动检查这些规则。
37. useCallback 和 useMemo 能否互换?
不能完全互换,但可以互相转换。
// useMemo 版本
const memoizedCallback = useMemo(() => fn(a, b), [a, b])
// useCallback 版本(等价于上面的 useMemo)
const memoizedCallback = useCallback(() => fn(a, b), [a, b])
区别在于语义清晰度:
useMemo(() => compute(a, b), [a, b])→ 语义:缓存计算结果useCallback(() => doSomething(a, b), [a, b])→ 语义:缓存函数
建议: 缓存函数用 useCallback,缓存值用 useMemo,保持代码意图清晰。
38. useState 的函数式更新是什么?为什么推荐?
// ❌ 依赖闭包中的 count(可能是旧值)
const increment = () => setCount(count + 1)
// ✅ 函数式更新:通过函数参数获取最新的 state
const increment = () => setCount(prev => prev + 1)
为什么推荐?
- 避免闭包陷阱:不依赖外部变量,永远是最新的 state
- 安全的并发更新:React 可以安全地合并多次更新
- 依赖数组更干净:useCallback/useEffect 中不需要把 state 加入依赖
// 经典场景:定时器中累加
useEffect(() => {
const timer = setInterval(() => {
// ❌ count 是闭包中的旧值,每次都是 0 + 1
// setCount(count + 1)
// ✅ 每次都基于最新的 count 递增
setCount(prev => prev + 1)
}, 1000)
return () => clearInterval(timer)
}, []) // 空依赖也能正常工作
39. useEffect 的清理函数有什么用?常见场景?
useEffect(() => {
// 副作用逻辑
const subscription = api.subscribe(handler)
// 清理函数:组件卸载或依赖变化前执行
return () => {
subscription.unsubscribe()
}
}, [])
常见清理场景:
// 1. 清除定时器
useEffect(() => {
const timer = setInterval(() => { /* ... */ }, 1000)
return () => clearInterval(timer)
}, [])
// 2. 取消网络请求
useEffect(() => {
const controller = new AbortController()
fetch(url, { signal: controller.signal })
return () => controller.abort()
}, [url])
// 3. 移除事件监听
useEffect(() => {
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
// 4. 断开 WebSocket
useEffect(() => {
const ws = new WebSocket(url)
return () => ws.close()
}, [url])
// 5. 取消订阅(MutationObserver / IntersectionObserver)
useEffect(() => {
const observer = new MutationObserver(callback)
observer.observe(node, config)
return () => observer.disconnect()
}, [])
第五部分:性能优化
40. React 性能优化的手段有哪些?
优化优先级(从高到低):
- 状态下沉——把状态放到真正需要它的组件中,减少不必要的渲染范围
- 组件拆分——大组件拆小,配合 React.memo 阻断渲染传播
- React.memo——对纯展示组件做浅比较,避免无效渲染
- useMemo / useCallback——缓存计算结果和函数引用
- 虚拟化列表——长列表只渲染可视区域(react-window / react-virtualized)
- 代码分割——lazy + Suspense 按需加载路由/组件
- 数据缓存——TanStack Query 缓存 API 数据,避免重复请求
- 避免在渲染中创建对象/数组——防止子组件 memo 失效
// ❌ 每次渲染都创建新对象,React.memo 失效
<Component style={{ color: 'red' }} />
// ✅ 提前定义常量
const style = useMemo(() => ({ color: 'red' }), [])
<Component style={style} />
41. 什么是虚拟列表?原理是什么?
虚拟列表(Virtual List)只渲染可视区域(viewport)内的列表项,而非全部数据。当用户滚动时,动态替换可见区域外的项目。
原理:
- 计算可视区域高度和每项高度
- 只渲染可视区域内的项目(加上上下 buffer)
- 监听滚动事件,更新渲染的项目范围
- 用
transform: translateY()定位到正确位置
// 使用 react-window
import { FixedSizeList } from 'react-window'
function VirtualList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
)}
</FixedSizeList>
)
}
适用场景: 列表项超过 100 条、无限滚动 Feed 流、大数据表格。
42. React 中如何避免不必要的重渲染?
排查方法:
// 使用 React DevTools Profiler 录制渲染
<Profiler id="MyComponent" onRender={(id, phase, actualDuration) => {
console.log(`${id} ${phase} took ${actualDuration}ms`)
}}>
<MyComponent />
</Profiler>
常见原因和解决方案:
| 原因 | 解决方案 |
|---|---|
| 父组件 state 变化导致子组件重渲染 | React.memo 包裹子组件 |
| Context value 变化导致所有消费者重渲染 | 拆分 Context / useMemo 缓存 value |
| 每次渲染创建新对象/数组作为 props | useMemo 缓存引用 |
| 每次渲染创建新函数作为 props | useCallback 缓存函数 |
| 匿名箭头函数作为事件处理器 | 提取为稳定的回调函数 |
43. React 的代码分割(Code Splitting)怎么实现?
import { lazy, Suspense } from 'react'
// 1. 懒加载组件
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
// 2. 配合 Suspense 使用
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
)
}
原理: lazy 利用动态 import() 语法,Webpack/Vite 会自动将导入的模块拆分为独立的 chunk 文件,只有在组件需要渲染时才加载对应的 JS。
注意事项:
- 必须有 fallback(Suspense 的 fallback 属性)
- 需要合理的错误边界(ChunkLoadError 处理)
- 不要过度拆分(每个 chunk 都有加载开销)
第六部分:React 原理与架构
44. React 的 Diff 算法是如何工作的?
React 通过三个策略将 O(n³) 的暴力 diff 降为 O(n):
策略一:Tree Diff(跨层级移动不可能)
- React 假设元素不会跨层级移动
- 只比较同一层级的节点
- 不同类型的组件直接替换整棵子树
策略二:Component Diff(不同类型直接替换)
// <div> → <span>:直接销毁 div 子树,创建 span 子树
// <ComponentA> → <ComponentB>:直接卸载 A,挂载 B
策略三:Element Diff(同层比较,通过 key 匹配)
// 旧
<ul>
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>
</ul>
// 新
<ul>
<li key="a">A</li>
<li key="d">D</li> {/* 新增 */}
<li key="b">B</li>
<li key="c">C</li>
{/* <li key="b">B</li> 的位置变了,但 key 相同,只需移动 */}
</ul>
Diff 的结果: 生成一组副作用(Effect),如 Placement(插入/移动)、Update(更新)、Deletion(删除)。
45. React 的调度机制(Scheduler)是怎样的?
React 18 引入了 Scheduler 模块,实现了任务的优先级调度。
任务优先级等级:
| 优先级 | Lane | 典型任务 |
|---|---|---|
| Immediate | SyncLane | 同步更新(flushSync) |
| User Blocking | InputContinuousLane | 用户输入(onClick、onChange) |
| Default | DefaultLanes | 普通更新(setTimeout、数据加载) |
| Idle | IdleLane | 空闲时执行(预渲染、分析) |
时间切片(Time Slicing):
- React 将大任务拆分为小的工作单元(Fiber)
- 每个工作单元执行前检查是否有更高优先级的任务
- 如果有,暂停当前任务,让高优先级任务先执行
- 完成后继续之前的任务
高优先级任务(用户输入):████
低优先级任务(数据渲染):░░░░ ████ ░░ ████
↑ ↑ ↑
暂停 恢复 暂停
46. React 的并发模式(Concurrent Mode)是什么?
并发模式是 React 18 的核心特性,允许 React 同时处理多个更新,并根据优先级决定哪些更新应该丢弃。
核心能力:
- 可中断渲染:Render 阶段可以被更高优先级任务打断
- 优先级调度:用户输入 > 动画 > 数据加载 > 预渲染
- useTransition:标记非紧急更新,可被中断
- useDeferredValue:延迟某个值,保持 UI 响应
function SearchPage() {
const [isPending, startTransition] = useTransition()
const [results, setResults] = useState([])
const handleChange = (e) => {
// 高优先级:立即响应用户输入
setSearchTerm(e.target.value)
// 低优先级:搜索请求可被中断
startTransition(() => {
setResults(searchResults(e.target.value))
})
}
return (
<div>
<input onChange={handleChange} />
{isPending && <Spinner />}
<ResultList results={results} />
</div>
)
}
47. React 的双缓冲机制是什么?
React 维护了两棵 Fiber 树:
- current 树:当前正在屏幕上显示的 UI
- workInProgress 树:正在内存中构建的新 UI
current 树(屏幕上的) workInProgress 树(内存中的)
App App
/ | \ / | \
A B C A B D
↑
正在构建新树
工作流程:
- 触发更新时,复制 current 树到 workInProgress 树
- 在 workInProgress 树上进行 Diff,生成新的 Fiber 树
- 切换指针:将 workInProgress 树变为新的 current 树
- Commit 阶段:将变更应用到真实 DOM
好处: 用户永远不会看到不完整的 UI(要么看到旧的,要么看到新的,不会看到中间的半成品)。
第七部分:生态与工程化
48. React Router v6 的核心变化?
| v5 | v6 | 说明 |
|---|---|---|
<Switch> | <Routes> | 路由容器改名 |
component / render prop | element prop | 路由定义方式改变 |
exact 属性 | 默认精确匹配 | 不再需要 exact |
useHistory | useNavigate | 导航 Hook 改变 |
| 无 | loader / action | 新增数据路由 API |
| 无 | useOutletContext | 嵌套路由数据传递 |
// v6 对象式配置
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
loader: (args) => fetch('/api/layout-data'),
children: [
{ index: true, element: <Home /> },
{ path: 'users/:userId', element: <UserDetail />, loader: userLoader },
]
}
])
// 组件中使用
function UserDetail() {
const { userId } = useParams()
const [searchParams] = useSearchParams()
const navigate = useNavigate()
const data = useLoaderData()
// ...
}
49. Next.js 的渲染方式有哪些?
| 渲染方式 | 全称 | 渲染时机 | 适用场景 |
|---|---|---|---|
| CSR | Client-Side Rendering | 浏览器端 | 管理后台、工具类应用 |
| SSR | Server-Side Rendering | 每次请求时服务端渲染 | 动态内容、SEO 要求高 |
| SSG | Static Site Generation | 构建时生成静态 HTML | 博客、文档站 |
| ISR | Incremental Static Regeneration | 构建时 + 定时重新生成 | 新闻、电商商品页 |
| RSC | React Server Components | 服务端组件按需渲染 | Next.js App Router 默认 |
// Next.js App Router
// 默认是 Server Component
async function ProductPage({ params }) {
const product = await db.products.findUnique({ where: { id: params.id } })
return <ProductDetail product={product} />
}
// 需要交互的组件标记为 Client Component
'use client'
function AddToCartButton({ productId }) {
return <button onClick={() => addToCart(productId)}>加入购物车</button>
}
50. React 项目的目录结构如何组织?
src/
├── components/ # 通用 UI 组件
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.module.css
│ │ └── Button.test.tsx
│ └── Modal/
├── features/ # 按功能模块组织
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── services/
│ │ └── store.ts
│ └── dashboard/
├── hooks/ # 通用自定义 Hook
├── lib/ # 工具函数
├── services/ # API 请求
├── store/ # 全局状态
├── types/ # TypeScript 类型定义
├── utils/ # 辅助函数
├── App.tsx
└── index.tsx
组织原则:
- 就近原则:相关代码放在一起(Feature-based)
- 组件独立性:每个组件有自己的文件夹(含样式、测试)
- 公共提取:被多处使用的代码提取到 common/shared 目录
第八部分:高级特性
51. React Server Components(RSC)是什么?
Server Components 是 React 18/19 引入的特性,允许组件只在服务端运行,其代码不会发送到客户端。
核心优势:
- 零 Bundle:Server Component 的 JS 不发送到客户端
- 直接访问后端资源:可以直接查询数据库、读取文件系统
- 自动代码分割:Client Component 会被自动分包
// Server Component(默认)—— 零 JS 发送到客户端
async function ProductList() {
const products = await db.products.findAll() // 直接查数据库
return (
<ul>
{products.map(p => (
<li key={p.id}>{p.name} - ¥{p.price}</li>
))}
</ul>
)
}
// Client Component —— 需要交互时才标记
'use client'
function AddToCartButton({ productId }) {
return <button onClick={() => handleAdd(productId)}>加入购物车</button>
}
Server vs Client 对比:
| 维度 | Server Component | Client Component |
|---|---|---|
| 运行环境 | 服务端 | 浏览器 |
| JS Bundle | 0 KB | 有大小 |
| 数据获取 | 直接查数据库/API | 通过 API 请求 |
| Hooks | 不支持 useState/useEffect | 支持所有 Hooks |
| 交互 | 不支持事件 | 支持事件处理 |
| 默认 | ✅(App Router) | 需标记 'use client' |
52. React 中的水合(Hydration)是什么?
水合是指将服务端渲染的静态 HTML "激活"为可交互的动态页面的过程。
服务端:生成静态 HTML(快,可直接发送给浏览器)
↓
客户端:下载 HTML → 下载 JS → Hydration(绑定事件监听器、恢复状态)
↓
可交互的动态页面
// 服务端渲染的 HTML(静态,SEO 友好)
<html>
<body>
<div id="root">
<button>点击我</button> <!-- 静态 HTML,还不能交互 -->
</div>
</body>
</html>
// 客户端水合后
<html>
<body>
<div id="root">
<button onclick="...">点击我</button> <!-- 绑定了事件,可交互 -->
</div>
</body>
</html>
面试加分点:
ReactDOM.hydrate()而非ReactDOM.render()- 水合不匹配(Hydration Mismatch):服务端和客户端渲染结果不一致时会报警告
- 常见 mismatch 原因:浏览器 API(window/document)、日期格式化、第三方脚本修改 DOM
53. React 的 Fiber 架构中,时间切片(Time Slicing)是如何实现的?
主线程时间线:
|-- 用户输入 --|-- React 工作 --|-- 浏览器绘制 --|-- React 工作 --|-- 绘制 --|
5ms 16ms 5ms 16ms
每个帧约 16ms(60fps),React 在每个帧的剩余时间里执行尽可能多的 Fiber 工作。
实现原理:
- React 在执行每个 Fiber 节点前,检查是否超过了分配的时间预算(deadline)
- 如果超时,暂停当前工作,将未完成的任务放入优先级队列
- 浏览器完成绘制后,React 继续处理剩余任务
- 如果有更高优先级的任务(如用户输入),优先执行
// 简化版时间切片逻辑
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
// 检查是否超时(每帧剩余时间不足)
shouldYield = deadline.timeRemaining() < 1
}
if (nextUnitOfWork) {
// 还有未完成的工作,请求下一次空闲时间继续
requestIdleCallback(workLoop)
} else {
// 所有工作完成,进入 Commit 阶段
commitRoot()
}
}
54. React 中如何实现无限滚动(Infinite Scroll)?
import { useEffect, useRef, useState } from 'react'
function InfiniteScroll({ loadMore, hasMore, children }) {
const [loading, setLoading] = useState(false)
const observerRef = useRef(null)
const sentinelRef = useRef(null)
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore && !loading) {
setLoading(true)
loadMore().finally(() => setLoading(false))
}
},
{ threshold: 0.1 } // 哨兵元素 10% 可见时触发
)
const sentinel = sentinelRef.current
if (sentinel) {
observer.observe(sentinel)
}
return () => observer.disconnect()
}, [hasMore, loading, loadMore])
return (
<div>
{children}
<div ref={sentinelRef} style={{ height: 1 }} /> {/* 哨兵元素 */}
{loading && <div>加载中...</div>}
{!hasMore && <div>没有更多了</div>}
</div>
)
}
// 使用
function Feed() {
const [page, setPage] = useState(1)
const [items, setItems] = useState([])
const [hasMore, setHasMore] = useState(true)
const loadMore = async () => {
const newItems = await fetchItems(page)
setItems(prev => [...prev, ...newItems])
setPage(p => p + 1)
setHasMore(newItems.length > 0)
}
return (
<InfiniteScroll loadMore={loadMore} hasMore={hasMore}>
{items.map(item => <Item key={item.id} data={item} />)}
</InfiniteScroll>
)
}
原理: 使用 IntersectionObserver 监听一个"哨兵元素",当它进入可视区域时触发加载更多。
55. React 中如何处理错误?
错误处理的层次:
┌─────────────────────────────────────┐
│ ErrorBoundary(捕获渲染错误) │
│ ┌───────────────────────────────┐ │
│ │ try-catch(捕获同步代码错误) │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ Promise.catch(异步错误)│ │ │
│ │ └─────────────────────────┘ │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
1. Error Boundary(渲染错误)
class ErrorFallback extends Component {
static getDerivedStateFromError(error) {
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
console.error('Caught an error:', error, errorInfo)
}
render() {
if (this.state.hasError) {
return <h1>出错了</h1>
}
return this.props.children
}
}
2. 事件处理中的错误
function Button() {
const handleClick = () => {
try {
riskyOperation()
} catch (error) {
console.error('事件处理出错:', error)
}
}
return <button onClick={handleClick}>按钮</button>
}
3. 异步操作中的错误
function DataFetcher() {
const [error, setError] = useState(null)
useEffect(() => {
fetchData()
.catch(err => setError(err.message))
}, [])
if (error) return <div>加载失败: {error}</div>
// ...
}
注意: Error Boundary 不能捕获事件处理和异步代码中的错误,这些需要 try-catch 或 .catch()。
56. React 中如何实现组件的过渡动画?
方式一:CSSTransition(react-transition-group)
import { CSSTransition } from 'react-transition-group'
function FadeComponent({ in: show }) {
return (
<CSSTransition
in={show}
timeout={300}
classNames="fade"
>
<div>Hello</div>
</CSSTransition>
)
}
方式二:Animate Presence(Framer Motion)
import { motion, AnimatePresence } from 'framer-motion'
function AnimatedModal({ isOpen }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
>
Modal Content
</motion.div>
)}
</AnimatePresence>
)
}
方式三:CSS + useState(简单场景)
function SlidePanel({ isOpen }) {
return (
<div className={`panel ${isOpen ? 'slide-in' : 'slide-out'}`}>
Content
</div>
)
}
57. React 应用中如何做国际化(i18n)?
// 使用 react-i18next
import { useTranslation } from 'react-i18next'
// 翻译文件
// locales/zh.json
{
"welcome": "欢迎",
"login": "登录",
"greeting": "你好,{{name}}"
}
function App() {
const { t, i18n } = useTranslation()
return (
<div>
<h1>{t('welcome')}</h1>
<p>{t('greeting', { name: '张三' })}</p>
<button onClick={() => i18n.changeLanguage('en')}>English</button>
</div>
)
}
关键考虑:
- 语言文件按模块拆分,按需加载
- 复数处理(
{{count}} 条消息vs{{count}} messages) - 日期、数字、货币的本地化格式
- RTL 语言支持(阿拉伯语等)
58. React 应用的监控和错误上报怎么做?
// 1. 全局错误边界 + 上报
class ErrorReporter extends Component {
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
// 上报到 Sentry / 自建监控平台
Sentry.captureException(error, { extra: errorInfo })
}
render() {
if (this.state.hasError) {
return <ErrorFallback />
}
return this.props.children
}
}
// 2. 性能监控
<Profiler id="Navigation" onRender={(id, phase, duration, metrics) => {
reportPerformance({ id, phase, duration, renderTime: metrics.selfTime })
}}>
<Page />
</Profiler>
// 3. 用户行为追踪
function trackClick(event) {
analytics.track('button_click', {
label: event.target.textContent,
page: window.location.pathname
})
}
59. React 中如何实现权限控制?
// 方式一:HOC(高阶组件)
function withAuth(Component) {
return function AuthenticatedComponent(props) {
const { isAuthenticated, user } = useAuth()
if (!isAuthenticated) return <Redirect to="/login" />
return <Component user={user} {...props} />
}
}
// 方式二:自定义 Hook
function usePermission(permission) {
const { user } = useAuth()
return user?.permissions.includes(permission) ?? false
}
function AdminPanel() {
const canEdit = usePermission('edit')
return canEdit ? <Editor /> : <Forbidden />
}
// 方式三:组件形式
function Can({ permission, children, fallback = <Forbidden /> }) {
const { user } = useAuth()
return user?.permissions.includes(permission) ? children : fallback
}
// 使用
<Can permission="delete">
<DeleteButton />
</Can>
60. React 应用的打包优化有哪些手段?
| 手段 | 效果 | 工具 |
|---|---|---|
| 代码分割 | 按需加载,减小首屏体积 | React.lazy + Suspense |
| Tree Shaking | 移除未使用的代码 | Webpack/Rollup/esbuild |
| 压缩 | 减小文件体积 | TerserPlugin / esbuild |
| 图片优化 | 转 WebP、懒加载、Base64 小图 | vite-plugin-imagemin |
| 依赖分析 | 找出大包来源 | rollup-plugin-visualizer |
| 缓存 | 二次访问秒开 | Service Worker / CDN |
| 预加载 | 提升关键资源加载速度 | <link rel="preload"> |
| SSR/SSG | 首屏直出 HTML | Next.js / Remix |
// Vite 配置示例
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom'],
'vendor-ui': ['antd'],
}
}
}
}
})
附录:面试高频速查表
| 分类 | 必会知识点 |
|---|---|
| 基础 | JSX、组件分类、props/state、单向数据流、声明式 |
| 生命周期 | mount/update/unmount、useEffect 依赖数组 |
| Hooks | useState、useEffect、useRef、useMemo、useCallback、自定义 Hook |
| 性能 | React.memo、useMemo、虚拟列表、代码分割、避免重渲染 |
| 原理 | Fiber、Diff、双缓冲、时间切片、合成事件 |
| 生态 | React Router、Redux/Zustand、Next.js、TanStack Query |
| 工程化 | 组件拆分、状态管理选型、错误处理、打包优化 |