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 的学习曲线相对平缓,但要精通需要理解其底层原理。
1// React 组件示例2import React, { useState } from 'react'34function Counter() {5 const [count, setCount] = useState(0)6 7 return (8 <div>9 <p>Count: {count}</p>10 <button onClick={() => setCount(count + 1)}>11 Increment12 </button>13 </div>14 )15}1617export default Counter1.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 的规则和闭包陷阱
- 某些场景下性能优化需要额外处理
- 类组件
- 函数组件
1import React, { Component } from 'react'23class Counter extends Component {4 constructor(props) {5 super(props)6 this.state = {7 count: 08 }9 // 需要绑定 this10 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}3031export default Counter1import React, { useState, useEffect } from 'react'23function Counter() {4 const [count, setCount] = useState(0)5 6 useEffect(() => {7 console.log('Component mounted')8 }, [])9 10 const handleClick = () => {11 setCount(count + 1)12 }13 14 return (15 <div>16 <p>Count: {count}</p>17 <button onClick={handleClick}>Increment</button>18 </div>19 )20}2122export 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 可以在编译时进行优化
- 生成更高效的代码
1// JSX 语法2const element = (3 <div className="container">4 <h1>Hello, {name}!</h1>5 <p>Welcome to React</p>6 </div>7)89// 编译后的 JavaScript10const 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 的规则:
- 必须有一个根元素(或使用 Fragment)
- 标签必须闭合
- 使用 className 代替 class
- 使用 htmlFor 代替 for
- 样式属性使用对象形式
- 注释使用
实际应用: 虽然 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 的规则:
- 只在顶层调用 Hooks:不要在循环、条件或嵌套函数中调用
- 只在 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,用于在函数组件中添加状态。
基本用法:
1const [state, setState] = useState(initialState)- state:当前状态值
- setState:更新状态的函数
- initialState:初始状态值
特点和注意事项:
1. 状态更新是异步的
- setState 不会立即更新状态
- React 会批量处理状态更新
- 如果需要基于前一个状态更新,使用函数式更新
2. 状态更新会触发重新渲染
- 当状态改变时,组件会重新渲染
- React 使用 Object.is 比较新旧状态
- 如果状态没有变化,不会触发重新渲染
3. 初始状态只在首次渲染时使用
- 后续渲染会忽略初始状态参数
- 如果初始状态计算复杂,可以传入函数(惰性初始化)
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]:依赖变化时执行
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. 无限循环
1// ❌ 错误:会导致无限循环2useEffect(() => {3 setCount(count + 1)4})56// ✅ 正确:添加依赖数组7useEffect(() => {8 setCount(count + 1)9}, [])2. 闭包陷阱
1// ❌ 问题:count 永远是初始值2useEffect(() => {3 const timer = setInterval(() => {4 setCount(count + 1)5 }, 1000)6 return () => clearInterval(timer)7}, [])89// ✅ 解决:使用函数式更新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
- 组件解耦
缺点:
- 过度使用会导致组件难以复用
- 性能问题(所有消费者都会重新渲染)
1// Context 使用示例2import { createContext, useContext, useState } from 'react'34// 创建 Context5const ThemeContext = createContext()67// 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}1819// 消费 Context20function Header() {21 const { theme, setTheme } = useContext(ThemeContext)22 23 return (24 <header className={theme}>25 <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>26 Toggle Theme27 </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):
-
constructor()
- 初始化 state
- 绑定事件处理函数
- 不要在这里调用 setState
-
static getDerivedStateFromProps()
- 根据 props 更新 state
- 返回对象更新 state,返回 null 不更新
- 很少使用
-
render()
- 必须实现的方法
- 返回 JSX
- 应该是纯函数
-
componentDidMount()
- 组件挂载后调用
- 适合进行 DOM 操作、数据获取、订阅事件
- 可以调用 setState
更新阶段(Updating):
-
static getDerivedStateFromProps()
- 同挂载阶段
-
shouldComponentUpdate()
- 返回 true/false 决定是否更新
- 用于性能优化
- PureComponent 自动实现浅比较
-
render()
- 重新渲染
-
getSnapshotBeforeUpdate()
- 在 DOM 更新前调用
- 返回值传给 componentDidUpdate
- 用于获取更新前的 DOM 信息
-
componentDidUpdate()
- 组件更新后调用
- 可以进行 DOM 操作
- 可以调用 setState(需要条件判断)
卸载阶段(Unmounting):
- componentWillUnmount()
- 组件卸载前调用
- 清理定时器、取消订阅、取消网络请求
- 不能调用 setState
错误处理:
-
static getDerivedStateFromError()
- 捕获子组件错误
- 返回新的 state
-
componentDidCatch()
- 记录错误信息
- 用于错误边界
4.2 Hooks 与生命周期的对应关系
面试回答思路:
函数组件使用 Hooks 来实现类组件的生命周期功能。
对应关系:
1// componentDidMount2useEffect(() => {3 console.log('mounted')4}, [])56// componentDidUpdate7useEffect(() => {8 console.log('updated')9})1011// componentWillUnmount12useEffect(() => {13 return () => {14 console.log('unmounted')15 }16}, [])1718// componentDidMount + componentDidUpdate19useEffect(() => {20 console.log('mounted or updated')21 22 return () => {23 console.log('cleanup before update or unmount')24 }25}, [dependency])特殊情况:
1// 模拟 componentDidUpdate(跳过首次渲染)2const isFirstRender = useRef(true)34useEffect(() => {5 if (isFirstRender.current) {6 isFirstRender.current = false7 return8 }9 10 console.log('updated only')11}, [dependency])五、性能优化
5.1 React 性能优化的方法
面试回答思路:
React 性能优化是面试的重点,我会从多个方面介绍优化方法:
1. 使用 React.memo
防止不必要的重新渲染。
1const MyComponent = React.memo(function MyComponent(props) {2 return <div>{props.value}</div>3})45// 自定义比较函数6const MyComponent = React.memo(7 function MyComponent(props) {8 return <div>{props.value}</div>9 },10 (prevProps, nextProps) => {11 return prevProps.value === nextProps.value12 }13)2. 使用 useMemo
缓存计算结果。
1const expensiveValue = useMemo(() => {2 return computeExpensiveValue(a, b)3}, [a, b])3. 使用 useCallback
缓存函数引用。
1const handleClick = useCallback(() => {2 doSomething(a, b)3}, [a, b])4. 代码分割和懒加载
1const LazyComponent = React.lazy(() => import('./LazyComponent'))23function App() {4 return (5 <Suspense fallback={<div>Loading...</div>}>6 <LazyComponent />7 </Suspense>8 )9}5. 虚拟列表
使用 react-window 或 react-virtualized 渲染大列表。
6. 避免内联对象和函数
1// ❌ 每次渲染都创建新对象2<Component style={{ color: 'red' }} />34// ✅ 提取到外部5const style = { color: 'red' }6<Component style={style} />7. 使用 key 优化列表渲染
1// ✅ 使用稳定的 key2{items.map(item => (3 <Item key={item.id} data={item} />4))}56// ❌ 使用索引作为 key(可能导致问题)7{items.map((item, index) => (8 <Item key={index} data={item} />9))}8. 使用 Fragment 避免额外的 DOM 节点
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 的结构:
1// JSX2<div className="container">3 <h1>Hello</h1>4 <p>World</p>5</div>67// 虚拟 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 的工作流程:
- 初始渲染时,创建虚拟 DOM 树
- 将虚拟 DOM 转换为真实 DOM
- 当状态改变时,创建新的虚拟 DOM 树
- 通过 Diff 算法比较新旧虚拟 DOM
- 计算出最小的变化
- 只更新变化的部分到真实 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. 元素类型不同
1// 旧2<div>3 <Counter />4</div>56// 新7<span>8 <Counter />9</span>1011// 结果:销毁 div 和 Counter,创建新的 span 和 Counter2. 元素类型相同
1// 旧2<div className="before" title="old" />34// 新5<div className="after" title="new" />67// 结果:保留 DOM 节点,只更新属性3. 组件类型相同
1// 旧2<Counter count={1} />34// 新5<Counter count={2} />67// 结果:保留组件实例,更新 props,触发 componentWillReceiveProps4. 列表 Diff(最复杂)
没有 key 的情况:
1// 旧2<ul>3 <li>A</li>4 <li>B</li>5</ul>67// 新(在开头插入 C)8<ul>9 <li>C</li>10 <li>A</li>11 <li>B</li>12</ul>1314// 结果:React 会依次比较,认为所有元素都变了15// 更新 A→C, B→A, 新建 B(效率低)有 key 的情况:
1// 旧2<ul>3 <li key="a">A</li>4 <li key="b">B</li>5</ul>67// 新8<ul>9 <li key="c">C</li>10 <li key="a">A</li>11 <li key="b">B</li>12</ul>1314// 结果:React 通过 key 识别出 A 和 B 没变,只需要新建 C(效率高)key 的最佳实践:
✅ 好的做法:
1// 使用稳定的唯一标识2{items.map(item => (3 <Item key={item.id} data={item} />4))}❌ 不好的做法:
1// 使用索引作为 key(列表顺序可能变化时)2{items.map((item, index) => (3 <Item key={index} data={item} />4))}56// 使用随机数作为 key7{items.map(item => (8 <Item key={Math.random()} data={item} />9))}为什么不推荐用索引作为 key:
当列表顺序改变时,索引会导致:
- 组件状态错乱
- 性能问题
- 不必要的重新渲染
Diff 算法的局限性:
- 跨层级移动节点效率低
- 需要开发者正确使用 key
- 某些场景下可能不如手动优化
实际应用建议:
- 保持 DOM 结构稳定
- 避免跨层级移动节点
- 为列表项提供稳定的 key
- 避免在渲染方法中创建新的组件类型
七、状态管理
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)
1// Action 示例2{3 type: 'ADD_TODO',4 payload: {5 id: 1,6 text: 'Learn Redux'7 }8}910// Action Creator11function addTodo(text) {12 return {13 type: 'ADD_TODO',14 payload: {15 id: Date.now(),16 text17 }18 }19}3. Reducer
- 纯函数,接收旧状态和 action,返回新状态
- 不能修改原状态,必须返回新对象
- 处理不同的 action type
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.id12 ? { ...todo, completed: !todo.completed }13 : todo14 )15 16 default:17 return state18 }19}4. Middleware
- 扩展 Redux 功能的方式
- 在 action 到达 reducer 之前进行拦截
- 常用于异步操作、日志记录等
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.data11 })12 } catch (error) {13 dispatch({14 type: 'FETCH_TODOS_FAILURE',15 payload: error.message16 })17 }18 }19}Redux 的数据流:
1用户交互2 ↓3dispatch(action)4 ↓5Middleware(可选)6 ↓7Reducer8 ↓9新的 State10 ↓11Store 更新12 ↓13组件重新渲染在 React 中使用 Redux:
1import { createStore } from 'redux'2import { Provider, useSelector, useDispatch } from 'react-redux'34// 1. 创建 Store5const store = createStore(rootReducer)67// 2. 提供 Store8function App() {9 return (10 <Provider store={store}>11 <TodoList />12 </Provider>13 )14}1516// 3. 使用 Store17function TodoList() {18 // 读取状态19 const todos = useSelector(state => state.todos)20 21 // 获取 dispatch22 const dispatch = useDispatch()23 24 // 触发 action25 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
- 不需要服务器配置
- 兼容性好
1import { BrowserRouter, HashRouter } from 'react-router-dom'23// History 模式4<BrowserRouter>5 <App />6</BrowserRouter>78// Hash 模式9<HashRouter>10 <App />11</HashRouter>2. 路由配置
1import { Routes, Route, Link } from 'react-router-dom'23function 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. 嵌套路由
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}1415function Layout() {16 return (17 <div>18 <nav>...</nav>19 <Outlet /> {/* 子路由渲染位置 */}20 </div>21 )22}4. 路由参数
1import { useParams, useSearchParams } from 'react-router-dom'23// 路径参数4function UserDetail() {5 const { id } = useParams()6 return <div>User ID: {id}</div>7}89// 查询参数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 Query19 </button>20 </div>21 )22}5. 编程式导航
1import { useNavigate } from 'react-router-dom'23function 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. 路由守卫
1import { Navigate } from 'react-router-dom'23// 受保护的路由4function ProtectedRoute({ children }) {5 const isAuthenticated = useAuth()6 7 if (!isAuthenticated) {8 return <Navigate to="/login" replace />9 }10 11 return children12}1314// 使用15<Route16 path="/dashboard"17 element={18 <ProtectedRoute>19 <Dashboard />20 </ProtectedRoute>21 }22/>7. 懒加载
1import { lazy, Suspense } from 'react'23const About = lazy(() => import('./pages/About'))4const Users = lazy(() => import('./pages/Users'))56function 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 的工作原理:
- 监听 URL 变化(History API 或 hashchange 事件)
- 匹配当前 URL 与路由配置
- 渲染对应的组件
- 提供导航 API
最佳实践:
- 使用 BrowserRouter(需要服务器配置)
- 合理使用嵌套路由
- 使用懒加载优化性能
- 实现路由守卫保护敏感页面
- 使用 Link 而不是 a 标签
九、常见问题
9.1 React 中的 key 为什么重要?
面试回答思路:
key 是 React 中一个非常重要但容易被忽视的概念。
key 的作用:
key 帮助 React 识别哪些元素改变了(添加、删除、修改)。它给予元素一个稳定的标识。
为什么需要 key:
- 提高 Diff 效率:React 可以快速找到对应的元素
- 保持组件状态:避免状态错乱
- 优化性能:减少不必要的 DOM 操作
key 的使用规则:
- key 在兄弟节点中必须唯一
- key 应该稳定、可预测
- 不要使用索引作为 key(列表顺序可能变化时)
- 不要使用随机数作为 key
错误示例和后果:
1// ❌ 使用索引作为 key2{items.map((item, index) => (3 <Item key={index} data={item} />4))}56// 问题:当列表顺序改变时7// 原来的 key=0 可能对应不同的数据8// 导致组件状态错乱正确示例:
1// ✅ 使用唯一标识作为 key2{items.map(item => (3 <Item key={item.id} data={item} />4))}9.2 React 中如何处理表单?
面试回答思路:
React 中处理表单有两种方式:受控组件和非受控组件。
受控组件(Controlled Components):
表单数据由 React 组件管理。
特点:
- 表单元素的值由 state 控制
- 每次输入都会触发状态更新
- 可以实时验证
- 符合 React 的数据流
1function Form() {2 const [formData, setFormData] = useState({3 username: '',4 email: '',5 password: ''6 })7 8 const handleChange = (e) => {9 const { name, value } = e.target10 setFormData(prev => ({11 ...prev,12 [name]: value13 }))14 }15 16 const handleSubmit = (e) => {17 e.preventDefault()18 console.log(formData)19 }20 21 return (22 <form onSubmit={handleSubmit}>23 <input24 name="username"25 value={formData.username}26 onChange={handleChange}27 />28 <input29 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 获取。
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.value10 })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 面试题库涵盖了:
- ✅ React 基础概念和核心特性
- ✅ 类组件 vs 函数组件
- ✅ JSX 语法
- ✅ Hooks 详解
- ✅ 组件通信
- ✅ 生命周期
- ✅ 性能优化
- ✅ 虚拟 DOM 和 Diff 算法
- ✅ Redux 状态管理
- ✅ React Router 路由
- ✅ 常见问题和最佳实践
建议结合实际项目经验,深入理解每个知识点的应用场景和最佳实践。
评论区 / Comments