前端面试知识库
框架专题

React 专题

从 Fiber 架构、Hooks 机制到并发渲染,整理 React 高频追问。

React 专题题考的不是 API 记忆,而是你是否理解这套框架如何调度更新、处理副作用和维护 UI 一致性。先讲架构原理,再讲 Hooks 机制,最后落到项目实践。

高频主线

  • Fiber 解决了什么问题,Reconciliation 如何工作
  • useEffectuseLayoutEffect 的差异和执行时机
  • 状态批处理和更新调度的优先级机制
  • 如何判断一个组件需要优化,优化顺序是什么

回答建议

  1. 先说 React 如何安排一次更新(调度 → 协调 → 提交)。
  2. 再解释 Hooks 的运行约束(调用顺序、闭包陷阱)。
  3. 最后补充项目里遇到的性能瓶颈与排查方式。

Fiber 架构

为什么需要 Fiber

React 15 的 Stack Reconciler 是递归同步的——一旦开始 diff 就不会停下来,长任务直接阻塞主线程,动画掉帧、输入卡顿。

Fiber 的核心目标是可中断、可恢复、可排优先级的协调过程。

Fiber 节点结构

interface Fiber {
  // 静态结构
  tag: WorkTag              // 函数组件 / 类组件 / 原生元素 ...
  type: any                 // 函数 / 类 / 标签名
  key: string | null

  // 树结构(链表代替递归栈)
  return: Fiber | null      // 父节点
  child: Fiber | null       // 第一个子节点
  sibling: Fiber | null     // 下一个兄弟节点

  // 工作状态
  alternate: Fiber | null  // 双缓冲:当前帧 ↔ 工作帧
  pendingProps: any
  memoizedProps: any
  memoizedState: any        // Hooks 链表头(函数组件)

  // 副作用
  flags: SideEffectFlags    // Placement / Update / Deletion ...
  subtreeFlags: number      // 子树副作用汇总(快速跳过无变化子树)
  updateQueue: UpdateQueue | null
}

双缓冲机制(Double Buffering)

current 树  ← 屏幕上正在显示的
    ↕  alternate
workInProgress 树  ← 正在构建的下一帧
  • 每次 setState 触发更新,React 从 current 复制出 workInProgress
  • 在 workInProgress 上完成协调和 diff
  • 完成后一次性交换指针:current = workInProgress
  • 用户看到的是完整的新帧,不会出现中间态

协调流程(Reconciliation)

调度阶段(Scheduler)

setState / dispatch 创建 Update 对象,Scheduler 根据优先级排队

协调阶段(Reconciler)— 可中断

workLoop 循环处理每个 Fiber 节点:

  • beginWork:自顶向下,创建/复用子 Fiber
  • completeWork:自底向上,标记副作用 flags
  • shouldYield:检查是否要让出主线程(高优任务到来或时间片耗尽)

提交阶段(Commit)— 不可中断

  • Before Mutation:读取 DOM 快照
  • Mutation:执行 DOM 操作(插入/更新/删除)
  • Layout:执行 useLayoutEffect / ref 回调

浏览器绘制

绘制完成后,异步执行 useEffect 回调

优先级与调度

优先级来源说明
ImmediateflushSync同步执行,不可中断
UserBlocking点击、输入16ms 内完成
NormaluseState / useReducer默认优先级
LowuseTransition可被高优打断
IdlerequestIdleCallback空闲时执行

高优先级更新可以打断低优先级更新。被打断的低优先级工作会被丢弃或重新开始,避免用户看到过期状态("饥饿"问题通过不断重试低优来解决)。

渲染与提交

三个阶段

阶段可中断可访问 DOM执行内容
Render(协调)构建 Fiber 树、计算 diff
Pre-commit是(只读)读取 DOM 快照
Commit执行 DOM 操作、执行 Layout 副作用

触发渲染的原因

  • setState / useReducer dispatch
  • forceUpdate(类组件)
  • 父组件重新渲染(子组件默认跟随)
  • Context 值变更会触发所有消费者渲染
  • useRef 变更不会触发渲染

Diff 算法

React 的协调基于三条假设(官方称"启发式"):

  1. 跨层级移动极少 → 同层比较,不跨层
  2. 不同类型 = 不同树 → 直接卸载重建
  3. Key 标识同一性 → 同类型 + 同 Key = 可复用
旧: [A, B, C, D]
新: [A, C, D, B]  (B 移到末尾)

React 的做法:
1. 从左到右逐个对比
2. A 匹配 → 复用
3. B ≠ C → 标记 B 删除,C 新建
4. 之后逐个处理剩余差异
5. 最后插入 B

结果: 删除 B + 新建 C + 新建 D + 新建 B
旧: [A, B, C, D, E]
新: [A, C, D, B, E]

Vue 3 的 LIS 算法:
1. 最长递增子序列: [A, C, D, E]
2. C、D 不动(在 LIS 中)
3. 只需移动 B 到末尾

结果: 移动 B(1 次 DOM 操作)

React 的列表 diff 不是最优算法,O(n) 而非最小移动数。Key 的核心作用是让 React 识别节点身份,避免"就地复用"导致的状态错乱。

Hooks 机制

调用规则

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

为什么有这些规则?Hooks 存储在 Fiber 的 memoizedState 链表上,React 按调用顺序匹配。条件分支导致顺序不一致,后面的 Hook 会对错节点。

Hooks 链表

// 每个 Hook 是链表节点
interface Hook {
  memoizedState: any    // 当前值
  queue: UpdateQueue    // 待更新队列(useState)
  next: Hook | null     // 下一个 Hook
}

// Fiber.memoizedState → Hook1 → Hook2 → Hook3 → null

useState / useReducer

// useState 本质是 useReducer 的语法糖
const [state, dispatch] = useReducer(basicStateReducer, initialState)

更新机制

dispatch(action)

创建 Update 对象,加入 Hook.queue 环形链表

调度渲染(scheduleUpdateOnFiber)

下次渲染时,从 queue 中取出所有 update,逐个 reduce

得到新 memoizedState

批处理

// 事件回调、Promise、setTimeout 等全部批处理
function handleClick() {
  setA(1)   // 不会立即渲染
  setB(2)   // 合并一次渲染
}

// Promise 中同样批处理
fetch('/api').then(() => {
  setA(1)
  setB(2)  // 仍然只渲染一次
})
// flushSync:同步渲染,立即提交
// 慎用!会退出批处理,阻塞后续更新
flushSync(() => {
  setC(3)  // 同步渲染
})
setD(4)    // 在 flushSync 完成后才处理

useEffect vs useLayoutEffect vs useInsertionEffect

维度useEffectuseLayoutEffectuseInsertionEffect
执行时机浏览器绘制之后DOM 更新后、绘制之前DOM 变更前
是否阻塞绘制
适用场景数据请求、事件绑定读取 DOM 布局、同步修改CSS-in-JS 注入样式
SSR 行为正常报 warning正常
Commit 阶段详细顺序:
├── Before Mutation
│   └── useInsertionEffect 回调
├── Mutation
│   └── 执行 DOM 操作(插入/更新/删除)
└── Layout
    ├── useLayoutEffect 回调
    ├── ref 回调
    └── ↓ 浏览器绘制后
        └── useEffect 回调(异步调度)

useLayoutEffect 中的 DOM 修改会在浏览器绘制前生效,用户看不到闪烁。useEffect 中的修改会触发额外一次重绘。优先用 useEffect,只在需要同步读取/修改 DOM 时用 useLayoutEffect

useEffect 闭包陷阱

function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const id = setInterval(() => {
      // 闭包捕获的是创建时的 count,永远是 0
      console.log(count)        // 0, 0, 0...
      setCount(count + 1)       // 始终变成 1
    }, 1000)
    return () => clearInterval(id)
  }, []) // 空依赖 → 闭包中的 count 永远是初始值
}
// 方案 1:函数式更新(推荐)
setCount(c => c + 1)  // c 是最新值

// 方案 2:useRef 持有最新值
const countRef = useRef(count)
countRef.current = count
// 在回调中使用 countRef.current

// 方案 3:将 count 加入依赖(每次重建定时器)
useEffect(() => {
  const id = setInterval(() => {
    console.log(count) // 正确
  }, 1000)
  return () => clearInterval(id)
}, [count])

useRef

// useRef 返回 { current: initialValue }
// 修改 .current 不触发渲染
// 在整个生命周期中保持引用稳定

// 典型用途
const inputRef = useRef<HTMLInputElement>(null)  // DOM 引用
const prevValueRef = useRef(props.value)          // 上一次的值
const timerRef = useRef<number>()                 // 存储 ID

useMemo / useCallback

// useMemo: 缓存计算结果
const sortedList = useMemo(
  () => list.sort(compareFn),
  [list, compareFn]
)

// useCallback: 缓存函数引用
const handleSubmit = useCallback(
  (e) => { /* ... */ },
  [deps]
)
// useCallback 本质是 useMemo 的语法糖
useCallback(fn, deps)
// 等价于
useMemo(() => fn, deps)

// 两者的目的都是避免子组件不必要渲染
// 本身不减少计算开销
// 需配合 React.memo 才有效

useContext

const ThemeContext = createContext<'light' | 'dark'>('light')

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Page />
    </ThemeContext.Provider>
  )
}

function Button() {
  const theme = useContext(ThemeContext) // "dark"
}

Context 值变化时,所有消费者都会重新渲染,无法跳过中间组件。解决方案:拆分 Context、用 React.memo 包裹消费者、或换用 Zustand/Jotai 等按需订阅的状态库。

自定义 Hook

// 命名:use 前缀
function useWindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 })

  useEffect(() => {
    const handler = () => setSize({ width: innerWidth, height: innerHeight })
    handler()
    window.addEventListener('resize', handler)
    return () => window.removeEventListener('resize', handler)
  }, [])

  return size
}

并发特性(React 18+)

useTransition

const [isPending, startTransition] = useTransition()

function handleSearch(input: string) {
  // 紧急更新:输入框立即响应
  setSearchInput(input)

  // 非紧急更新:搜索结果可以延迟
  startTransition(() => {
    setSearchQuery(input) // 可被点击/输入等高优更新打断
  })
}

useDeferredValue

const [search, setSearch] = useState('')
const deferredSearch = useDeferredValue(search)

// deferredSearch 是 search 的延迟版本
// 当有更高优先级更新时,deferredSearch 保持旧值
// 空闲时自动更新为新值

Suspense 与流式 SSR

<Suspense fallback={<Spinner />}>
  <AsyncData /> {/* 内部 throw Promise 触发 Suspense */}
</Suspense>
SSR 流式渲染流程:
├── 服务端发送 HTML 骨架
├── 遇到 Suspense 边界 → 先发送 fallback
├── 异步数据就绪 → 流式发送真实内容(替换 fallback)
└── 客户端 Hydration 同样按 Suspense 边界渐进

Server Components(RSC)

Server vs Client Components

维度Server ComponentsClient Components
运行环境仅服务端客户端(也可在服务端 SSR)
bundle 包含不包含包含
交互性无(无 useState/useEffect)
数据获取直接 async/awaituseEffect / SWR / React Query
API 调用可直连数据库需走 API 层
体积影响零(不会发送到客户端)正常计入 bundle
// Server Component(默认,无需声明)
// 可直接 async/await 获取数据
// 可直连数据库,零客户端体积
async function ProductPage({ id }: { id: string }) {
  const product = await db.query(
    'SELECT * FROM products WHERE id = ?',
    [id]
  )
  return <ProductDetail product={product} />
}
// Client Component(显式声明 'use client')
// 可使用全部 Hooks 和事件处理
'use client'
function AddToCart({ productId }: { productId: string }) {
  const [loading, setLoading] = useState(false)

  async function handleAdd() {
    setLoading(true)
    await api.addToCart(productId)
    setLoading(false)
  }

  return <button onClick={handleAdd}>加入购物车</button>
}

RSC 数据流规则

Server Component
  ├── 可直接 async/await 获取数据
  ├── 可导入其他 Server Component
  ├── 可将 Client Component 作为子组件渲染
  └── 不可使用 useState / useEffect / 事件处理

Client Component
  ├── 可使用全部 Hooks
  ├── 可处理用户交互
  ├── 不可导入 Server Component
  │   (可接收 Server Component 作为 children / props)
  └── 数据通过 props / Client Hook 获取

Server Component 传 Client Component 的正确方式:通过 children prop。不要在 Client Component 内 import Server Component。

性能优化

优化优先级(从高到低)

  1. 减少不必要的渲染 — 找到重渲染的组件,修正数据流
  2. 减少计算量 — 拆分重计算到 useMemo
  3. 减少 DOM 操作 — 虚拟化长列表
  4. 减少 bundle 体积 — 代码分割 + lazy loading

React.memo

const ExpensiveList = React.memo(
  function List({ items, onClick }: Props) {
    return items.map(item => (
      <li key={item.id} onClick={() => onClick(item.id)}>
        {item.name}
      </li>
    ))
  }
)
// 自定义浅比较(慎用,容易遗漏)
const List = React.memo(
  function List(props) { /* ... */ },
  (prev, next) => {
    return prev.items === next.items
      && prev.onClick === next.onClick
  }
)

优化手段对比

手段目标开销适用场景
React.memo避免子组件渲染浅比较 propsprops 变化少的展示组件
useMemo缓存计算结果依赖比较昂贵的派生计算
useCallback缓存函数引用依赖比较传给 memo 子组件的回调
useRef持有可变引用几乎无不触发渲染的值
代码分割减小首屏 bundle额外网络请求路由级 / 大组件
虚拟列表减少 DOM 节点计算可见区域超长列表

不要过度优化。先确认组件确实存在性能问题(React DevTools Profiler),再针对性优化。过早加 memo/useMemo 反而增加依赖比较开销。

代码分割

// React.lazy + Suspense
const HeavyChart = React.lazy(() => import('./HeavyChart'))

function Dashboard() {
  return (
    <Suspense fallback={<Skeleton />}>
      <HeavyChart />
    </Suspense>
  )
}

列表虚拟化

import { FixedSizeList } from 'react-window'

function BigList({ items }) {
  return (
    <FixedSizeList height={600} itemCount={items.length} itemSize={50}>
      {({ index, style }) => (
        <div style={style}>{items[index].name}</div>
      )}
    </FixedSizeList>
  )
}

状态管理方案

方案对比

方案定位适用规模TS 支持学习成本
useState组件内状态原生
useReducer复杂组件内状态原生
useContext跨组件共享原生
Zustand轻量全局状态原生
Jotai原子化状态原生
Redux Toolkit企业级状态良好
React Query服务端状态缓存任意原生

何时引入全局状态库

组件内状态 → useState/useReducer
├── 跨层级但范围小 → useContext + useReducer
│   └── 问题:Context 值变 → 所有消费者重渲染
├── 多组件频繁共享 → Zustand / Jotai
│   └── 优势:按订阅粒度更新,不重渲染无关组件
└── 大型应用 + 时间旅行 → Redux Toolkit
    └── 配合 RTK Query 可统一服务端状态

服务端数据优先用 React Query / SWR / RTK Query,不要放入全局状态库。它们自带缓存、去重、失效、乐观更新。

错误边界

class ErrorBoundary extends React.Component<
  { children: React.ReactNode; fallback?: React.ReactNode },
  { hasError: boolean; error?: Error }
> {
  state = { hasError: false, error: undefined }

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error }
  }

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    logErrorToService(error, info.componentStack)
  }

  render() {
    if (this.state.hasError) return this.props.fallback ?? <h1>出错了</h1>
    return this.props.children
  }
}

// 使用
<ErrorBoundary fallback={<ErrorPage />}>
  <App />
</ErrorBoundary>

错误边界必须是类组件getDerivedStateFromError / componentDidCatch)。Hooks 目前无等价 API。它只捕获子组件渲染期间抛出的错误,不捕获事件处理、异步代码、SSR 中的错误。

React 设计模式

模式React 应用重要程度
观察者模式状态订阅、Hooks 依赖追踪核心
代理模式Fiber 节点代理真实 DOM核心
策略模式不同 WorkTag 的 beginWork 处理核心
组合模式children / Compound Components核心
中介者模式事件委托(SyntheticEvent)重要
工厂模式createElement / jsx重要
单例模式全局状态 Store一般
装饰器模式HOC(高阶组件)一般

常见误区

误区正解
useMemo 能提升所有性能依赖比较本身有开销,简单计算不需要
useCallback 能防止子组件重渲染需配合 React.memo 才有效
useEffect = componentDidMountuseEffect 在绘制后执行,且依赖变化时重新执行
多个 useState 一定触发多次渲染React 18 自动批处理,事件回调内只渲染一次
Context 能替代所有状态管理Context 无按需更新,消费者全量重渲染
RSC 能完全替代 SSRRSC 是组件级流式,SSR 是页面级,场景不同
flushSync 是优化手段它是退出批处理的逃生舱,慎用,会同步阻塞渲染

项目题结合点

  • 长列表 + 搜索场景如何避免输入卡顿(useDeferredValue + 虚拟列表)
  • 复杂表单如何拆分状态(组件内 useReducer + Context 分层)
  • 大型应用如何做代码分割(路由级 lazy + 按需加载状态库)
  • 如何排查不必要的重渲染(React DevTools Profiler + why-did-you-render

本页内容