React 基础面试题

65 阅读19分钟

React 面试题大全

本文档《React 高频面试题》(26 题)基础上,补充了大量基础面试题,供大家参考。


第一部分:React 基础概念

1. React 的核心概念是什么?

核心思想:UI = f(state)

核心概念说明
JSXJavaScript 语法扩展,编译为 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)?

  1. 可预测性:数据变化路径清晰,便于调试和追踪
  2. 性能:避免双向绑定带来的额外监听和更新开销
  3. 简单性:不需要维护复杂的依赖关系图

如果需要子组件修改父组件数据怎么办?

  • 通过回调函数:父组件传递 onUpdate 回调给子组件,子组件调用回调触发更新
  • 使用状态管理库(Redux、Zustand 等)

4. 函数组件和类组件的区别?

维度函数组件类组件
写法纯函数,返回 JSXES6 类,继承 Component
this无 this(或指向 undefined)有 this,指向组件实例
状态管理useState Hookthis.state + setState
生命周期useEffect 模拟componentDidMount/Update/Unmount 等
性能略优(无实例开销)略差(需要创建实例)
逻辑复用自定义 HookHOC / 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 事件差异。

工作原理:

  1. React 在组件树根部(React 17+ 挂在 root 节点,之前挂在 document)注册原生事件监听器
  2. 所有子组件的事件都会冒泡到这个根节点
  3. 根节点根据事件类型和目标,分发到对应的 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 引入的新协调引擎,解决了旧版递归渲染的两个问题:

  1. 不可中断:旧版递归一旦开始就不能暂停,大型组件树会导致主线程长时间阻塞
  2. 优先级无法区分:所有更新同等对待,用户输入等高优先级更新可能被低优先级更新阻塞

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>

作用:

  1. 避免不必要的 DOM 嵌套(减少对 CSS 布局的影响)
  2. 解决列表渲染中缺少单一父元素的警告
  3. 提高性能(少一层 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、绑定方法
挂载componentDidMountDOM 已挂载,发请求、订阅事件
更新componentDidUpdateDOM 已更新,操作更新后的 DOM
卸载componentWillUnmount清理定时器、取消订阅

函数组件对应的 Hook

类组件函数组件
constructor + componentDidMountuseEffect(() => {}, [])
componentDidUpdateuseEffect(() => {}, [deps])
componentWillUnmountuseEffect(() => { return cleanup }, [])
shouldComponentUpdateReact.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 的区别?

维度useEffectuseLayoutEffect
执行时机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 判断哪些元素是同一个元素(可以复用),哪些需要删除或新建。

三大规则:

  1. 稳定——同一数据生命周期内 key 不变
  2. 唯一——同层兄弟节点间唯一
  3. 可预测——不要用 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 组件何时应该拆分?

拆分原则: 单一职责——一个组件只做一件事。

具体信号:

  1. 组件过大:超过 200-300 行,难以阅读和维护
  2. 逻辑过重:包含多种不相关的业务逻辑
  3. 重复渲染:某部分 UI 频繁更新,但父组件其余部分不需要更新
  4. 复用需求:同一 UI 在多处使用
  5. 测试困难:组件功能太多,难以编写单元测试

拆分方向:

  • UI 拆分:提取子组件(如 Header、Sidebar、Content)
  • 逻辑拆分:提取自定义 Hook(如 useAuth、useFetch)
  • 数据拆分:将全局状态移到状态管理库

29. React 中受控组件和非受控组件的区别?

维度受控组件非受控组件
数据驱动React stateDOM 自身
值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?

维度useStateuseReducer
适用场景简单状态(单个值)复杂状态(多个子值相互关联)
更新方式setState(newValue)dispatch({ type, payload })
状态逻辑更新逻辑在组件内更新逻辑集中在 reducer 函数中
调试只能看最终值可追溯每次 dispatch
性能每次更新都可能触发重渲染可根据 action 决定是否 bailout

什么时候用 useReducer?

  1. 状态逻辑复杂(多个子状态相互依赖)
  2. 需要根据前一个状态计算下一个状态
  3. 需要在多个组件间共享相同的更新逻辑
  4. 需要频繁测试状态更新逻辑
// 复杂表单场景
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 的核心概念和工作原理?

三个核心概念:

  1. Store:单一数据源,整个应用的状态存在一个对象中
  2. Action:描述"发生了什么"的普通对象,必须有 type 字段
  3. 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)是什么?

  1. 只在顶层调用 Hook——不要在循环、条件语句、嵌套函数或 return 之后使用 Hook
  2. 只在 React 函数中调用 Hook——不要在普通 JS 函数中使用
  3. 自定义 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)

为什么推荐?

  1. 避免闭包陷阱:不依赖外部变量,永远是最新的 state
  2. 安全的并发更新:React 可以安全地合并多次更新
  3. 依赖数组更干净: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 性能优化的手段有哪些?

优化优先级(从高到低):

  1. 状态下沉——把状态放到真正需要它的组件中,减少不必要的渲染范围
  2. 组件拆分——大组件拆小,配合 React.memo 阻断渲染传播
  3. React.memo——对纯展示组件做浅比较,避免无效渲染
  4. useMemo / useCallback——缓存计算结果和函数引用
  5. 虚拟化列表——长列表只渲染可视区域(react-window / react-virtualized)
  6. 代码分割——lazy + Suspense 按需加载路由/组件
  7. 数据缓存——TanStack Query 缓存 API 数据,避免重复请求
  8. 避免在渲染中创建对象/数组——防止子组件 memo 失效
// ❌ 每次渲染都创建新对象,React.memo 失效
<Component style={{ color: 'red' }} />

// ✅ 提前定义常量
const style = useMemo(() => ({ color: 'red' }), [])
<Component style={style} />

41. 什么是虚拟列表?原理是什么?

虚拟列表(Virtual List)只渲染可视区域(viewport)内的列表项,而非全部数据。当用户滚动时,动态替换可见区域外的项目。

原理:

  1. 计算可视区域高度和每项高度
  2. 只渲染可视区域内的项目(加上上下 buffer)
  3. 监听滚动事件,更新渲染的项目范围
  4. 用 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
每次渲染创建新对象/数组作为 propsuseMemo 缓存引用
每次渲染创建新函数作为 propsuseCallback 缓存函数
匿名箭头函数作为事件处理器提取为稳定的回调函数

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典型任务
ImmediateSyncLane同步更新(flushSync)
User BlockingInputContinuousLane用户输入(onClick、onChange)
DefaultDefaultLanes普通更新(setTimeout、数据加载)
IdleIdleLane空闲时执行(预渲染、分析)

时间切片(Time Slicing):

  • React 将大任务拆分为小的工作单元(Fiber)
  • 每个工作单元执行前检查是否有更高优先级的任务
  • 如果有,暂停当前任务,让高优先级任务先执行
  • 完成后继续之前的任务
高优先级任务(用户输入):████
低优先级任务(数据渲染):░░░░  ████  ░░  ████
                          ↑      ↑     ↑
                        暂停   恢复  暂停

46. React 的并发模式(Concurrent Mode)是什么?

并发模式是 React 18 的核心特性,允许 React 同时处理多个更新,并根据优先级决定哪些更新应该丢弃。

核心能力:

  1. 可中断渲染:Render 阶段可以被更高优先级任务打断
  2. 优先级调度:用户输入 > 动画 > 数据加载 > 预渲染
  3. useTransition:标记非紧急更新,可被中断
  4. 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
                                       ↑
                                  正在构建新树

工作流程:

  1. 触发更新时,复制 current 树到 workInProgress 树
  2. 在 workInProgress 树上进行 Diff,生成新的 Fiber 树
  3. 切换指针:将 workInProgress 树变为新的 current 树
  4. Commit 阶段:将变更应用到真实 DOM

好处: 用户永远不会看到不完整的 UI(要么看到旧的,要么看到新的,不会看到中间的半成品)。


第七部分:生态与工程化

48. React Router v6 的核心变化?

v5v6说明
<Switch><Routes>路由容器改名
component / render propelement prop路由定义方式改变
exact 属性默认精确匹配不再需要 exact
useHistoryuseNavigate导航 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 的渲染方式有哪些?

渲染方式全称渲染时机适用场景
CSRClient-Side Rendering浏览器端管理后台、工具类应用
SSRServer-Side Rendering每次请求时服务端渲染动态内容、SEO 要求高
SSGStatic Site Generation构建时生成静态 HTML博客、文档站
ISRIncremental Static Regeneration构建时 + 定时重新生成新闻、电商商品页
RSCReact 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 引入的特性,允许组件只在服务端运行,其代码不会发送到客户端。

核心优势:

  1. 零 Bundle:Server Component 的 JS 不发送到客户端
  2. 直接访问后端资源:可以直接查询数据库、读取文件系统
  3. 自动代码分割: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 ComponentClient Component
运行环境服务端浏览器
JS Bundle0 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 工作。

实现原理:

  1. React 在执行每个 Fiber 节点前,检查是否超过了分配的时间预算(deadline)
  2. 如果超时,暂停当前工作,将未完成的任务放入优先级队列
  3. 浏览器完成绘制后,React 继续处理剩余任务
  4. 如果有更高优先级的任务(如用户输入),优先执行
// 简化版时间切片逻辑
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首屏直出 HTMLNext.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 依赖数组
HooksuseState、useEffect、useRef、useMemo、useCallback、自定义 Hook
性能React.memo、useMemo、虚拟列表、代码分割、避免重渲染
原理Fiber、Diff、双缓冲、时间切片、合成事件
生态React Router、Redux/Zustand、Next.js、TanStack Query
工程化组件拆分、状态管理选型、错误处理、打包优化