Skip to main content

React 面试题库

一、React 基础

1.1 什么是 React?它的核心特性是什么?

面试回答思路:

React 是由 Facebook 开发的一个用于构建用户界面的 JavaScript 库。我可以从以下几个方面介绍它的核心特性:

1. 组件化

  • React 采用组件化的开发模式,将 UI 拆分成独立、可复用的组件
  • 每个组件都有自己的状态和生命周期
  • 组件可以嵌套、组合,构建复杂的用户界面
  • 这种模式提高了代码的可维护性和复用性

2. 虚拟 DOM(Virtual DOM)

  • React 使用虚拟 DOM 来提高性能
  • 当状态改变时,React 会创建新的虚拟 DOM 树
  • 通过 Diff 算法比较新旧虚拟 DOM 的差异
  • 只更新真正需要改变的部分,减少了 DOM 操作

3. 单向数据流

  • 数据从父组件流向子组件(通过 props)
  • 子组件不能直接修改父组件的数据
  • 这种设计让数据流向更加清晰,便于调试和维护
  • 通过回调函数实现子组件向父组件通信

4. JSX 语法

  • JSX 是 JavaScript 的语法扩展
  • 可以在 JavaScript 中直接编写类似 HTML 的代码
  • 提高了代码的可读性和开发效率
  • 最终会被编译成 React.createElement 调用

5. 声明式编程

  • 只需要描述 UI 应该是什么样子
  • React 会自动处理 DOM 更新
  • 相比命令式编程,代码更简洁、更易理解

补充说明: React 的生态系统非常丰富,有 React Router(路由)、Redux/MobX(状态管理)、Next.js(SSR 框架)等配套工具。React 的学习曲线相对平缓,但要精通需要理解其底层原理。

jsx
1// React 组件示例
2import React, { useState } from 'react'
3
4function Counter() {
5 const [count, setCount] = useState(0)
6
7 return (
8 <div>
9 <p>Count: {count}</p>
10 <button onClick={() => setCount(count + 1)}>
11 Increment
12 </button>
13 </div>
14 )
15}
16
17export default Counter

1.2 React 类组件和函数组件的区别

面试回答思路:

React 有两种定义组件的方式:类组件和函数组件。随着 Hooks 的引入,函数组件已经成为主流。

类组件(Class Component):

特点:

  • 使用 ES6 class 语法定义
  • 必须继承 React.Component
  • 有完整的生命周期方法
  • 有 this 关键字,需要注意 this 绑定问题
  • 有 state 和 setState

优点:

  • 生命周期方法清晰明确
  • 可以使用 shouldComponentUpdate 优化性能

缺点:

  • 代码相对冗长
  • this 绑定容易出错
  • 逻辑复用困难(需要 HOC 或 Render Props)

函数组件(Function Component):

特点:

  • 使用普通函数定义
  • 没有 this
  • 通过 Hooks 管理状态和副作用
  • 代码更简洁

优点:

  • 代码简洁,易于理解
  • 没有 this 绑定问题
  • 通过 Hooks 实现逻辑复用
  • 更容易进行代码分割和优化
  • 更符合函数式编程思想

缺点:

  • 需要理解 Hooks 的规则和闭包陷阱
  • 某些场景下性能优化需要额外处理
jsx
1import React, { Component } from 'react'
2
3class Counter extends Component {
4 constructor(props) {
5 super(props)
6 this.state = {
7 count: 0
8 }
9 // 需要绑定 this
10 this.handleClick = this.handleClick.bind(this)
11 }
12
13 componentDidMount() {
14 console.log('Component mounted')
15 }
16
17 handleClick() {
18 this.setState({ count: this.state.count + 1 })
19 }
20
21 render() {
22 return (
23 <div>
24 <p>Count: {this.state.count}</p>
25 <button onClick={this.handleClick}>Increment</button>
26 </div>
27 )
28 }
29}
30
31export default Counter

选择建议:

  • 新项目推荐使用函数组件 + Hooks
  • 老项目可以逐步迁移,两种方式可以共存
  • 理解两种方式的差异有助于维护老代码

1.3 什么是 JSX?为什么使用 JSX?

面试回答思路:

JSX 是 JavaScript XML 的缩写,是 React 的核心语法特性之一。

JSX 的本质:

JSX 不是一种新的语言,而是 JavaScript 的语法扩展。它允许我们在 JavaScript 中编写类似 HTML 的代码,最终会被 Babel 编译成 React.createElement 调用。

为什么使用 JSX:

1. 提高可读性

  • JSX 的语法更接近 HTML,更直观
  • 开发者可以快速理解组件的结构
  • 降低了学习成本

2. 更好的开发体验

  • IDE 可以提供更好的代码提示和错误检查
  • 可以使用 HTML 的标签和属性
  • 支持 JavaScript 表达式

3. 防止注入攻击

  • React DOM 会自动转义 JSX 中的值
  • 防止 XSS(跨站脚本)攻击
  • 提高了应用的安全性

4. 优化编译

  • Babel 可以在编译时进行优化
  • 生成更高效的代码
jsx
1// JSX 语法
2const element = (
3 <div className="container">
4 <h1>Hello, {name}!</h1>
5 <p>Welcome to React</p>
6 </div>
7)
8
9// 编译后的 JavaScript
10const element = React.createElement(
11 'div',
12 { className: 'container' },
13 React.createElement('h1', null, 'Hello, ', name, '!'),
14 React.createElement('p', null, 'Welcome to React')
15)

JSX 的规则:

  1. 必须有一个根元素(或使用 Fragment)
  2. 标签必须闭合
  3. 使用 className 代替 class
  4. 使用 htmlFor 代替 for
  5. 样式属性使用对象形式
  6. 注释使用

实际应用: 虽然 JSX 不是必须的,但它已经成为 React 开发的标准。理解 JSX 的编译过程有助于理解 React 的工作原理和性能优化。


二、React Hooks

2.1 什么是 Hooks?为什么引入 Hooks?

面试回答思路:

Hooks 是 React 16.8 引入的新特性,它让函数组件也能使用状态和其他 React 特性。

为什么引入 Hooks:

1. 解决类组件的问题

  • 类组件中的 this 绑定容易出错
  • 生命周期方法中经常包含不相关的逻辑
  • 相关的逻辑被拆分到不同的生命周期方法中
  • 代码复用困难,需要 HOC 或 Render Props

2. 更好的逻辑复用

  • 通过自定义 Hooks 可以轻松复用状态逻辑
  • 不需要改变组件结构
  • 避免了 HOC 和 Render Props 的嵌套地狱

3. 更简洁的代码

  • 函数组件比类组件更简洁
  • 没有 this,代码更容易理解
  • 更符合函数式编程思想

4. 更好的代码组织

  • 可以按照功能组织代码,而不是按照生命周期
  • 相关的逻辑可以放在一起
  • 提高了代码的可维护性

Hooks 的规则:

  1. 只在顶层调用 Hooks:不要在循环、条件或嵌套函数中调用
  2. 只在 React 函数中调用 Hooks:在函数组件或自定义 Hooks 中调用

常用的 Hooks:

  • useState:管理状态
  • useEffect:处理副作用
  • useContext:使用 Context
  • useReducer:复杂状态管理
  • useCallback:缓存函数
  • useMemo:缓存计算结果
  • useRef:访问 DOM 或保存可变值
  • useImperativeHandle:自定义暴露给父组件的实例值
  • useLayoutEffect:同步执行副作用
  • useDebugValue:自定义 Hook 的调试标签

实际应用: Hooks 已经成为 React 开发的主流方式。理解 Hooks 的原理和使用规则,对于编写高质量的 React 代码至关重要。


2.2 useState 的使用和原理

面试回答思路:

useState 是最基础也是最常用的 Hook,用于在函数组件中添加状态。

基本用法:

jsx
1const [state, setState] = useState(initialState)
  • state:当前状态值
  • setState:更新状态的函数
  • initialState:初始状态值

特点和注意事项:

1. 状态更新是异步的

  • setState 不会立即更新状态
  • React 会批量处理状态更新
  • 如果需要基于前一个状态更新,使用函数式更新

2. 状态更新会触发重新渲染

  • 当状态改变时,组件会重新渲染
  • React 使用 Object.is 比较新旧状态
  • 如果状态没有变化,不会触发重新渲染

3. 初始状态只在首次渲染时使用

  • 后续渲染会忽略初始状态参数
  • 如果初始状态计算复杂,可以传入函数(惰性初始化)
jsx
1// 基本用法
2function Counter() {
3 const [count, setCount] = useState(0)
4
5 // 直接更新
6 const increment = () => {
7 setCount(count + 1)
8 }
9
10 // 函数式更新(推荐)
11 const incrementFunc = () => {
12 setCount(prevCount => prevCount + 1)
13 }
14
15 // 惰性初始化
16 const [data, setData] = useState(() => {
17 return expensiveComputation()
18 })
19
20 return <button onClick={increment}>Count: {count}</button>
21}

原理简述:

useState 内部维护了一个状态队列,每次调用 useState 都会按顺序返回对应的状态。这就是为什么 Hooks 必须在顶层调用,不能在条件语句中使用。


2.3 useEffect 的使用和原理

面试回答思路:

useEffect 用于处理副作用,相当于类组件中的生命周期方法。

基本概念:

副作用是指那些与渲染无关的操作,比如:

  • 数据获取
  • 订阅事件
  • 手动修改 DOM
  • 定时器
  • 日志记录

useEffect 的特点:

1. 默认在每次渲染后执行

  • 包括首次渲染和每次更新
  • 可以通过依赖数组控制执行时机

2. 支持清理函数

  • 返回一个函数用于清理副作用
  • 在组件卸载或下次 effect 执行前调用
  • 用于取消订阅、清除定时器等

3. 依赖数组

  • 空数组 []:只在挂载时执行一次
  • 不传:每次渲染都执行
  • [dep1, dep2]:依赖变化时执行
jsx
1function Example() {
2 const [count, setCount] = useState(0)
3
4 // 每次渲染都执行
5 useEffect(() => {
6 document.title = `Count: ${count}`
7 })
8
9 // 只在挂载时执行
10 useEffect(() => {
11 console.log('Component mounted')
12 }, [])
13
14 // 依赖变化时执行
15 useEffect(() => {
16 console.log('Count changed:', count)
17 }, [count])
18
19 // 带清理函数
20 useEffect(() => {
21 const timer = setInterval(() => {
22 console.log('Tick')
23 }, 1000)
24
25 return () => {
26 clearInterval(timer)
27 }
28 }, [])
29
30 return <div>{count}</div>
31}

与生命周期的对应关系:

  • componentDidMount:useEffect(() => , [])
  • componentDidUpdate:useEffect(() => )
  • componentWillUnmount:useEffect 的返回函数

常见问题和解决方案:

1. 无限循环

jsx
1// ❌ 错误:会导致无限循环
2useEffect(() => {
3 setCount(count + 1)
4})
5
6// ✅ 正确:添加依赖数组
7useEffect(() => {
8 setCount(count + 1)
9}, [])

2. 闭包陷阱

jsx
1// ❌ 问题:count 永远是初始值
2useEffect(() => {
3 const timer = setInterval(() => {
4 setCount(count + 1)
5 }, 1000)
6 return () => clearInterval(timer)
7}, [])
8
9// ✅ 解决:使用函数式更新
10useEffect(() => {
11 const timer = setInterval(() => {
12 setCount(c => c + 1)
13 }, 1000)
14 return () => clearInterval(timer)
15}, [])

三、组件通信

3.1 React 组件间通信的方式

面试回答思路:

React 组件间通信根据组件关系的不同,有多种实现方式:

1. Props(父传子)

最基本的通信方式,通过 props 传递数据。

特点:

  • 单向数据流
  • 类型安全(配合 TypeScript 或 PropTypes)
  • 简单直观

2. 回调函数(子传父)

父组件传递回调函数给子组件,子组件调用回调函数传递数据。

3. Context API(跨层级)

用于跨越多层组件传递数据,避免 props 逐层传递。

适用场景:

  • 主题配置
  • 用户信息
  • 语言设置
  • 全局状态

优点:

  • 避免 props drilling
  • 组件解耦

缺点:

  • 过度使用会导致组件难以复用
  • 性能问题(所有消费者都会重新渲染)
jsx
1// Context 使用示例
2import { createContext, useContext, useState } from 'react'
3
4// 创建 Context
5const ThemeContext = createContext()
6
7// Provider 组件
8function App() {
9 const [theme, setTheme] = useState('light')
10
11 return (
12 <ThemeContext.Provider value={{ theme, setTheme }}>
13 <Header />
14 <Content />
15 </ThemeContext.Provider>
16 )
17}
18
19// 消费 Context
20function Header() {
21 const { theme, setTheme } = useContext(ThemeContext)
22
23 return (
24 <header className={theme}>
25 <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
26 Toggle Theme
27 </button>
28 </header>
29 )
30}

4. Redux / Zustand / MobX(全局状态管理)

用于管理复杂的应用状态。

Redux 特点:

  • 单一数据源
  • 状态只读
  • 使用纯函数修改状态
  • 可预测的状态管理
  • 强大的调试工具

Zustand 特点:

  • 更简单的 API
  • 更小的包体积
  • 不需要 Provider
  • 支持 TypeScript

5. 自定义 Hooks

封装可复用的状态逻辑。

6. Event Bus(不推荐)

通过事件总线实现组件通信,但不符合 React 的设计理念。

选择建议:

  • 父子组件:Props 和回调函数
  • 跨层级:Context API
  • 全局状态:Redux/Zustand
  • 逻辑复用:自定义 Hooks

四、生命周期

4.1 React 生命周期方法(类组件)

面试回答思路:

虽然现在主要使用函数组件和 Hooks,但理解类组件的生命周期仍然很重要。

挂载阶段(Mounting):

  1. constructor()

    • 初始化 state
    • 绑定事件处理函数
    • 不要在这里调用 setState
  2. static getDerivedStateFromProps()

    • 根据 props 更新 state
    • 返回对象更新 state,返回 null 不更新
    • 很少使用
  3. render()

    • 必须实现的方法
    • 返回 JSX
    • 应该是纯函数
  4. componentDidMount()

    • 组件挂载后调用
    • 适合进行 DOM 操作、数据获取、订阅事件
    • 可以调用 setState

更新阶段(Updating):

  1. static getDerivedStateFromProps()

    • 同挂载阶段
  2. shouldComponentUpdate()

    • 返回 true/false 决定是否更新
    • 用于性能优化
    • PureComponent 自动实现浅比较
  3. render()

    • 重新渲染
  4. getSnapshotBeforeUpdate()

    • 在 DOM 更新前调用
    • 返回值传给 componentDidUpdate
    • 用于获取更新前的 DOM 信息
  5. componentDidUpdate()

    • 组件更新后调用
    • 可以进行 DOM 操作
    • 可以调用 setState(需要条件判断)

卸载阶段(Unmounting):

  1. componentWillUnmount()
    • 组件卸载前调用
    • 清理定时器、取消订阅、取消网络请求
    • 不能调用 setState

错误处理:

  1. static getDerivedStateFromError()

    • 捕获子组件错误
    • 返回新的 state
  2. componentDidCatch()

    • 记录错误信息
    • 用于错误边界

4.2 Hooks 与生命周期的对应关系

面试回答思路:

函数组件使用 Hooks 来实现类组件的生命周期功能。

对应关系:

jsx
1// componentDidMount
2useEffect(() => {
3 console.log('mounted')
4}, [])
5
6// componentDidUpdate
7useEffect(() => {
8 console.log('updated')
9})
10
11// componentWillUnmount
12useEffect(() => {
13 return () => {
14 console.log('unmounted')
15 }
16}, [])
17
18// componentDidMount + componentDidUpdate
19useEffect(() => {
20 console.log('mounted or updated')
21
22 return () => {
23 console.log('cleanup before update or unmount')
24 }
25}, [dependency])

特殊情况:

jsx
1// 模拟 componentDidUpdate(跳过首次渲染)
2const isFirstRender = useRef(true)
3
4useEffect(() => {
5 if (isFirstRender.current) {
6 isFirstRender.current = false
7 return
8 }
9
10 console.log('updated only')
11}, [dependency])

五、性能优化

5.1 React 性能优化的方法

面试回答思路:

React 性能优化是面试的重点,我会从多个方面介绍优化方法:

1. 使用 React.memo

防止不必要的重新渲染。

jsx
1const MyComponent = React.memo(function MyComponent(props) {
2 return <div>{props.value}</div>
3})
4
5// 自定义比较函数
6const MyComponent = React.memo(
7 function MyComponent(props) {
8 return <div>{props.value}</div>
9 },
10 (prevProps, nextProps) => {
11 return prevProps.value === nextProps.value
12 }
13)

2. 使用 useMemo

缓存计算结果。

jsx
1const expensiveValue = useMemo(() => {
2 return computeExpensiveValue(a, b)
3}, [a, b])

3. 使用 useCallback

缓存函数引用。

jsx
1const handleClick = useCallback(() => {
2 doSomething(a, b)
3}, [a, b])

4. 代码分割和懒加载

jsx
1const LazyComponent = React.lazy(() => import('./LazyComponent'))
2
3function App() {
4 return (
5 <Suspense fallback={<div>Loading...</div>}>
6 <LazyComponent />
7 </Suspense>
8 )
9}

5. 虚拟列表

使用 react-window 或 react-virtualized 渲染大列表。

6. 避免内联对象和函数

jsx
1// ❌ 每次渲染都创建新对象
2<Component style={{ color: 'red' }} />
3
4// ✅ 提取到外部
5const style = { color: 'red' }
6<Component style={style} />

7. 使用 key 优化列表渲染

jsx
1// ✅ 使用稳定的 key
2{items.map(item => (
3 <Item key={item.id} data={item} />
4))}
5
6// ❌ 使用索引作为 key(可能导致问题)
7{items.map((item, index) => (
8 <Item key={index} data={item} />
9))}

8. 使用 Fragment 避免额外的 DOM 节点

jsx
1return (
2 <>
3 <ChildA />
4 <ChildB />
5 </>
6)

9. 使用 Production 构建

确保生产环境使用压缩和优化的代码。

10. 使用 React DevTools Profiler

分析组件渲染性能,找出性能瓶颈。


六、虚拟 DOM 与 Diff 算法

6.1 什么是虚拟 DOM?为什么需要虚拟 DOM?

面试回答思路:

虚拟 DOM 是 React 的核心概念之一,理解它对于理解 React 的工作原理至关重要。

什么是虚拟 DOM:

虚拟 DOM(Virtual DOM)是真实 DOM 的 JavaScript 对象表示。它是一个轻量级的 JavaScript 对象树,用来描述真实 DOM 的结构。

为什么需要虚拟 DOM:

1. 性能优化

  • 直接操作 DOM 很慢,因为 DOM 操作会触发浏览器的重排和重绘
  • 虚拟 DOM 可以批量更新,减少 DOM 操作次数
  • 通过 Diff 算法找出最小变化,只更新必要的部分

2. 跨平台能力

  • 虚拟 DOM 是平台无关的
  • 可以渲染到不同的平台(Web、Native、Canvas 等)
  • React Native 就是基于这个特性实现的

3. 声明式编程

  • 开发者只需要描述 UI 应该是什么样子
  • React 负责将虚拟 DOM 转换为真实 DOM
  • 提高了开发效率和代码可维护性

4. 更好的开发体验

  • 不需要手动操作 DOM
  • 代码更简洁、更易理解
  • 便于实现时间旅行、撤销重做等功能

虚拟 DOM 的结构:

javascript
1// JSX
2<div className="container">
3 <h1>Hello</h1>
4 <p>World</p>
5</div>
6
7// 虚拟 DOM 对象
8{
9 type: 'div',
10 props: {
11 className: 'container',
12 children: [
13 {
14 type: 'h1',
15 props: {
16 children: 'Hello'
17 }
18 },
19 {
20 type: 'p',
21 props: {
22 children: 'World'
23 }
24 }
25 ]
26 }
27}

虚拟 DOM 的工作流程:

  1. 初始渲染时,创建虚拟 DOM 树
  2. 将虚拟 DOM 转换为真实 DOM
  3. 当状态改变时,创建新的虚拟 DOM 树
  4. 通过 Diff 算法比较新旧虚拟 DOM
  5. 计算出最小的变化
  6. 只更新变化的部分到真实 DOM

常见误解:

虚拟 DOM 并不总是比直接操作 DOM 快。在某些场景下(如大量简单的 DOM 操作),直接操作 DOM 可能更快。虚拟 DOM 的优势在于:

  • 提供了更好的开发体验
  • 在大多数场景下性能足够好
  • 避免了手动优化的复杂性

6.2 React 的 Diff 算法原理

面试回答思路:

React 的 Diff 算法是其性能优化的核心,理解它有助于写出更高效的代码。

传统 Diff 算法的问题:

传统的树形结构 Diff 算法的时间复杂度是 O(n³),对于大型应用来说性能无法接受。

React Diff 算法的优化策略:

React 通过三个假设将复杂度降低到 O(n):

1. 同层比较

  • 只比较同一层级的节点
  • 不会跨层级比较
  • 如果节点跨层级移动,会被视为删除和新建

2. 不同类型的元素会产生不同的树

  • 如果元素类型改变(如 div 变成 span),会销毁旧树,创建新树
  • 不会尝试复用

3. 通过 key 标识元素

  • 开发者可以通过 key 提示哪些元素是稳定的
  • 相同 key 的元素会被复用
  • 提高列表渲染的效率

Diff 算法的具体过程:

1. 元素类型不同

jsx
1// 旧
2<div>
3 <Counter />
4</div>
5
6// 新
7<span>
8 <Counter />
9</span>
10
11// 结果:销毁 div 和 Counter,创建新的 span 和 Counter

2. 元素类型相同

jsx
1// 旧
2<div className="before" title="old" />
3
4// 新
5<div className="after" title="new" />
6
7// 结果:保留 DOM 节点,只更新属性

3. 组件类型相同

jsx
1// 旧
2<Counter count={1} />
3
4// 新
5<Counter count={2} />
6
7// 结果:保留组件实例,更新 props,触发 componentWillReceiveProps

4. 列表 Diff(最复杂)

没有 key 的情况:

jsx
1// 旧
2<ul>
3 <li>A</li>
4 <li>B</li>
5</ul>
6
7// 新(在开头插入 C)
8<ul>
9 <li>C</li>
10 <li>A</li>
11 <li>B</li>
12</ul>
13
14// 结果:React 会依次比较,认为所有元素都变了
15// 更新 A→C, B→A, 新建 B(效率低)

有 key 的情况:

jsx
1// 旧
2<ul>
3 <li key="a">A</li>
4 <li key="b">B</li>
5</ul>
6
7// 新
8<ul>
9 <li key="c">C</li>
10 <li key="a">A</li>
11 <li key="b">B</li>
12</ul>
13
14// 结果:React 通过 key 识别出 A 和 B 没变,只需要新建 C(效率高)

key 的最佳实践:

✅ 好的做法:

jsx
1// 使用稳定的唯一标识
2{items.map(item => (
3 <Item key={item.id} data={item} />
4))}

❌ 不好的做法:

jsx
1// 使用索引作为 key(列表顺序可能变化时)
2{items.map((item, index) => (
3 <Item key={index} data={item} />
4))}
5
6// 使用随机数作为 key
7{items.map(item => (
8 <Item key={Math.random()} data={item} />
9))}

为什么不推荐用索引作为 key:

当列表顺序改变时,索引会导致:

  • 组件状态错乱
  • 性能问题
  • 不必要的重新渲染

Diff 算法的局限性:

  1. 跨层级移动节点效率低
  2. 需要开发者正确使用 key
  3. 某些场景下可能不如手动优化

实际应用建议:

  1. 保持 DOM 结构稳定
  2. 避免跨层级移动节点
  3. 为列表项提供稳定的 key
  4. 避免在渲染方法中创建新的组件类型

七、状态管理

7.1 Redux 的核心概念和工作原理

面试回答思路:

Redux 是 React 生态中最流行的状态管理库,虽然现在有很多替代方案,但理解 Redux 仍然很重要。

Redux 的三大原则:

1. 单一数据源(Single Source of Truth)

  • 整个应用的状态存储在一个对象树中
  • 这个对象树只存在于唯一的 store 中
  • 便于调试和状态追踪

2. 状态是只读的(State is Read-Only)

  • 唯一改变状态的方法是触发 action
  • 不能直接修改状态
  • 确保状态的可预测性

3. 使用纯函数进行修改(Changes are Made with Pure Functions)

  • 使用 reducer 来描述状态如何改变
  • reducer 是纯函数,相同的输入总是产生相同的输出
  • 不能有副作用

Redux 的核心概念:

1. Store

  • 保存应用状态的容器
  • 提供 getState() 获取状态
  • 提供 dispatch(action) 触发状态更新
  • 提供 subscribe(listener) 注册监听器

2. Action

  • 描述发生了什么的普通对象
  • 必须有 type 属性
  • 可以携带数据(payload)
javascript
1// Action 示例
2{
3 type: 'ADD_TODO',
4 payload: {
5 id: 1,
6 text: 'Learn Redux'
7 }
8}
9
10// Action Creator
11function addTodo(text) {
12 return {
13 type: 'ADD_TODO',
14 payload: {
15 id: Date.now(),
16 text
17 }
18 }
19}

3. Reducer

  • 纯函数,接收旧状态和 action,返回新状态
  • 不能修改原状态,必须返回新对象
  • 处理不同的 action type
javascript
1function todoReducer(state = [], action) {
2 switch (action.type) {
3 case 'ADD_TODO':
4 return [...state, action.payload]
5
6 case 'REMOVE_TODO':
7 return state.filter(todo => todo.id !== action.payload.id)
8
9 case 'TOGGLE_TODO':
10 return state.map(todo =>
11 todo.id === action.payload.id
12 ? { ...todo, completed: !todo.completed }
13 : todo
14 )
15
16 default:
17 return state
18 }
19}

4. Middleware

  • 扩展 Redux 功能的方式
  • 在 action 到达 reducer 之前进行拦截
  • 常用于异步操作、日志记录等
javascript
1// Redux Thunk 中间件示例
2function fetchTodos() {
3 return async (dispatch) => {
4 dispatch({ type: 'FETCH_TODOS_REQUEST' })
5
6 try {
7 const response = await api.getTodos()
8 dispatch({
9 type: 'FETCH_TODOS_SUCCESS',
10 payload: response.data
11 })
12 } catch (error) {
13 dispatch({
14 type: 'FETCH_TODOS_FAILURE',
15 payload: error.message
16 })
17 }
18 }
19}

Redux 的数据流:

1用户交互
2
3dispatch(action)
4
5Middleware(可选)
6
7Reducer
8
9新的 State
10
11Store 更新
12
13组件重新渲染

在 React 中使用 Redux:

jsx
1import { createStore } from 'redux'
2import { Provider, useSelector, useDispatch } from 'react-redux'
3
4// 1. 创建 Store
5const store = createStore(rootReducer)
6
7// 2. 提供 Store
8function App() {
9 return (
10 <Provider store={store}>
11 <TodoList />
12 </Provider>
13 )
14}
15
16// 3. 使用 Store
17function TodoList() {
18 // 读取状态
19 const todos = useSelector(state => state.todos)
20
21 // 获取 dispatch
22 const dispatch = useDispatch()
23
24 // 触发 action
25 const handleAdd = (text) => {
26 dispatch(addTodo(text))
27 }
28
29 return (
30 <div>
31 {todos.map(todo => (
32 <div key={todo.id}>{todo.text}</div>
33 ))}
34 <button onClick={() => handleAdd('New Todo')}>Add</button>
35 </div>
36 )
37}

Redux 的优缺点:

优点:

  • 状态可预测
  • 易于调试(Redux DevTools)
  • 支持时间旅行
  • 中间件系统强大
  • 社区成熟

缺点:

  • 样板代码多
  • 学习曲线陡峭
  • 对于简单应用过于复杂
  • 异步处理需要额外的中间件

现代替代方案:

  • Redux Toolkit:简化 Redux 使用
  • Zustand:更简单的状态管理
  • Jotai:原子化状态管理
  • Recoil:Facebook 的状态管理库

八、React Router

8.1 React Router 的基本使用和原理

面试回答思路:

React Router 是 React 应用中最常用的路由库,理解它的工作原理对于构建单页应用很重要。

React Router 的核心概念:

1. 路由模式

BrowserRouter(History 模式):

  • 使用 HTML5 History API
  • URL 格式:/user/123
  • 需要服务器配置支持
  • SEO 友好

HashRouter(Hash 模式):

  • 使用 URL 的 hash 部分
  • URL 格式:/#/user/123
  • 不需要服务器配置
  • 兼容性好
jsx
1import { BrowserRouter, HashRouter } from 'react-router-dom'
2
3// History 模式
4<BrowserRouter>
5 <App />
6</BrowserRouter>
7
8// Hash 模式
9<HashRouter>
10 <App />
11</HashRouter>

2. 路由配置

jsx
1import { Routes, Route, Link } from 'react-router-dom'
2
3function App() {
4 return (
5 <div>
6 <nav>
7 <Link to="/">Home</Link>
8 <Link to="/about">About</Link>
9 <Link to="/users">Users</Link>
10 </nav>
11
12 <Routes>
13 <Route path="/" element={<Home />} />
14 <Route path="/about" element={<About />} />
15 <Route path="/users" element={<Users />} />
16 <Route path="/users/:id" element={<UserDetail />} />
17 <Route path="*" element={<NotFound />} />
18 </Routes>
19 </div>
20 )
21}

3. 嵌套路由

jsx
1function App() {
2 return (
3 <Routes>
4 <Route path="/" element={<Layout />}>
5 <Route index element={<Home />} />
6 <Route path="about" element={<About />} />
7 <Route path="users" element={<Users />}>
8 <Route path=":id" element={<UserDetail />} />
9 </Route>
10 </Route>
11 </Routes>
12 )
13}
14
15function Layout() {
16 return (
17 <div>
18 <nav>...</nav>
19 <Outlet /> {/* 子路由渲染位置 */}
20 </div>
21 )
22}

4. 路由参数

jsx
1import { useParams, useSearchParams } from 'react-router-dom'
2
3// 路径参数
4function UserDetail() {
5 const { id } = useParams()
6 return <div>User ID: {id}</div>
7}
8
9// 查询参数
10function SearchPage() {
11 const [searchParams, setSearchParams] = useSearchParams()
12 const query = searchParams.get('q')
13
14 return (
15 <div>
16 <p>Search: {query}</p>
17 <button onClick={() => setSearchParams({ q: 'new query' })}>
18 Update Query
19 </button>
20 </div>
21 )
22}

5. 编程式导航

jsx
1import { useNavigate } from 'react-router-dom'
2
3function LoginPage() {
4 const navigate = useNavigate()
5
6 const handleLogin = () => {
7 // 登录成功后跳转
8 navigate('/dashboard')
9
10 // 替换当前历史记录
11 navigate('/dashboard', { replace: true })
12
13 // 返回上一页
14 navigate(-1)
15
16 // 传递状态
17 navigate('/dashboard', { state: { from: 'login' } })
18 }
19
20 return <button onClick={handleLogin}>Login</button>
21}

6. 路由守卫

jsx
1import { Navigate } from 'react-router-dom'
2
3// 受保护的路由
4function ProtectedRoute({ children }) {
5 const isAuthenticated = useAuth()
6
7 if (!isAuthenticated) {
8 return <Navigate to="/login" replace />
9 }
10
11 return children
12}
13
14// 使用
15<Route
16 path="/dashboard"
17 element={
18 <ProtectedRoute>
19 <Dashboard />
20 </ProtectedRoute>
21 }
22/>

7. 懒加载

jsx
1import { lazy, Suspense } from 'react'
2
3const About = lazy(() => import('./pages/About'))
4const Users = lazy(() => import('./pages/Users'))
5
6function App() {
7 return (
8 <Suspense fallback={<div>Loading...</div>}>
9 <Routes>
10 <Route path="/about" element={<About />} />
11 <Route path="/users" element={<Users />} />
12 </Routes>
13 </Suspense>
14 )
15}

React Router 的工作原理:

  1. 监听 URL 变化(History API 或 hashchange 事件)
  2. 匹配当前 URL 与路由配置
  3. 渲染对应的组件
  4. 提供导航 API

最佳实践:

  1. 使用 BrowserRouter(需要服务器配置)
  2. 合理使用嵌套路由
  3. 使用懒加载优化性能
  4. 实现路由守卫保护敏感页面
  5. 使用 Link 而不是 a 标签

九、常见问题

9.1 React 中的 key 为什么重要?

面试回答思路:

key 是 React 中一个非常重要但容易被忽视的概念。

key 的作用:

key 帮助 React 识别哪些元素改变了(添加、删除、修改)。它给予元素一个稳定的标识。

为什么需要 key:

  1. 提高 Diff 效率:React 可以快速找到对应的元素
  2. 保持组件状态:避免状态错乱
  3. 优化性能:减少不必要的 DOM 操作

key 的使用规则:

  1. key 在兄弟节点中必须唯一
  2. key 应该稳定、可预测
  3. 不要使用索引作为 key(列表顺序可能变化时)
  4. 不要使用随机数作为 key

错误示例和后果:

jsx
1// ❌ 使用索引作为 key
2{items.map((item, index) => (
3 <Item key={index} data={item} />
4))}
5
6// 问题:当列表顺序改变时
7// 原来的 key=0 可能对应不同的数据
8// 导致组件状态错乱

正确示例:

jsx
1// ✅ 使用唯一标识作为 key
2{items.map(item => (
3 <Item key={item.id} data={item} />
4))}

9.2 React 中如何处理表单?

面试回答思路:

React 中处理表单有两种方式:受控组件和非受控组件。

受控组件(Controlled Components):

表单数据由 React 组件管理。

特点:

  • 表单元素的值由 state 控制
  • 每次输入都会触发状态更新
  • 可以实时验证
  • 符合 React 的数据流
jsx
1function Form() {
2 const [formData, setFormData] = useState({
3 username: '',
4 email: '',
5 password: ''
6 })
7
8 const handleChange = (e) => {
9 const { name, value } = e.target
10 setFormData(prev => ({
11 ...prev,
12 [name]: value
13 }))
14 }
15
16 const handleSubmit = (e) => {
17 e.preventDefault()
18 console.log(formData)
19 }
20
21 return (
22 <form onSubmit={handleSubmit}>
23 <input
24 name="username"
25 value={formData.username}
26 onChange={handleChange}
27 />
28 <input
29 name="email"
30 type="email"
31 value={formData.email}
32 onChange={handleChange}
33 />
34 <button type="submit">Submit</button>
35 </form>
36 )
37}

非受控组件(Uncontrolled Components):

表单数据由 DOM 管理,通过 ref 获取。

jsx
1function Form() {
2 const usernameRef = useRef()
3 const emailRef = useRef()
4
5 const handleSubmit = (e) => {
6 e.preventDefault()
7 console.log({
8 username: usernameRef.current.value,
9 email: emailRef.current.value
10 })
11 }
12
13 return (
14 <form onSubmit={handleSubmit}>
15 <input ref={usernameRef} />
16 <input ref={emailRef} type="email" />
17 <button type="submit">Submit</button>
18 </form>
19 )
20}

选择建议:

  • 大多数情况使用受控组件
  • 文件上传等特殊场景使用非受控组件
  • 需要实时验证时使用受控组件

十、总结

这份 React 面试题库涵盖了:

  1. ✅ React 基础概念和核心特性
  2. ✅ 类组件 vs 函数组件
  3. ✅ JSX 语法
  4. ✅ Hooks 详解
  5. ✅ 组件通信
  6. ✅ 生命周期
  7. ✅ 性能优化
  8. ✅ 虚拟 DOM 和 Diff 算法
  9. ✅ Redux 状态管理
  10. ✅ React Router 路由
  11. ✅ 常见问题和最佳实践

建议结合实际项目经验,深入理解每个知识点的应用场景和最佳实践。

forum

评论区 / Comments