React 一面:生命周期——从 class 到 Hooks 与常见面试题

React 相关的一面题里,生命周期几乎是必考点。
这一篇不打算按照 API 文档逐个罗列,而是想搞清楚几件事:React 生命周期到底在解决什么问题、class 时代和 Hooks 时代分别怎么表达,以及在面试里哪些题目最容易混淆。

先别急着背 componentDidMountuseEffect 这些名字,可以先把组件想象成这样一个过程:

  • 初次挂载到页面上(mount)
  • 随着 props/state 变化不断更新(update)
  • 最后从页面上被移除(unmount)

React 提供的各种生命周期钩子,本质上就是在这三个阶段的 关键时间点 给你一个机会:

  • 在组件第一次出现时做初始化工作
    • 比如:发起首个请求、订阅事件、测量 DOM。
  • 在依赖变化时做同步/副作用更新
    • 比如:根据新的 props 拉取数据、同步外部系统状态。
  • 在组件销毁前做清理
    • 比如:移除事件监听、清理定时器、取消请求。

不管是 class 组件还是函数组件,只是表达方式变了,解决的问题其实一直是这三类。

如果用最常见的 class 组件来画一条「时间轴」,大致是这样几段:

  • 挂载阶段(mount)
    • constructor
    • componentDidMount
  • 更新阶段(update)
    • componentDidUpdate
  • 卸载阶段(unmount)
    • componentWillUnmount

在面试里,常问的几个点可以这样理解:

  • constructor
    • 更偏向于初始化 state / 绑定方法,不建议在这里做副作用(如请求、DOM 操作)。
  • componentDidMount
    • 组件第一次真实挂到 DOM 上后调用,适合:发起网络请求、订阅事件、操作真实 DOM。
  • componentDidUpdate
    • 每次更新后触发,适合:根据 props/state 变化去同步外部状态,但要注意条件判断,否则非常容易死循环。
  • componentWillUnmount
    • 组件从 DOM 上移除前调用,适合:清理定时器、解绑事件监听、取消请求等。

面试常见问法通常不是「你能背出几个生命周期吗」,而是:

  • 「如果要在组件销毁前清理一个订阅,放在哪个生命周期合适?」
  • 「如果要在 props 更新后根据新值拉一次数据,你会用哪个生命周期?怎么避免多余请求?」

这些问题本质上都是在考:你能不能把业务动作放在正确的时间点上,并控制好触发频率。

函数组件出来之后,React 官方更推荐用 Hooks,这时候「生命周期」不再是一连串命名 API,而是通过 useEffect 这种钩子来表达「什么时候做什么」。

可以把 useEffect 理解成三个组合在一起的东西:

  • 依赖列表(依赖谁的变化)
  • 副作用函数(变化后要做什么)
  • 清理函数(组件卸载或依赖变化前要做什么)

一个典型模式是:

1useEffect(() => {
2  // 副作用逻辑:如发请求、订阅事件等
3
4  return () => {
5    // 清理逻辑:如取消订阅、清理定时器等
6  };
7}, [dep1, dep2]);

和 class 生命周期对照来看,可以大致这样映射:

  • 只在挂载时执行一次的逻辑
    • class:componentDidMount
    • Hooks:useEffect(() => { ... }, [])
  • 在依赖变化时执行的逻辑
    • class:componentDidUpdate + 手动判断 prevProps / prevState
    • Hooks:useEffect(() => { ... }, [dep])
  • 在卸载前执行清理逻辑
    • class:componentWillUnmount
    • Hooks:useEffect 返回的清理函数

真正的差异在于:Hooks 把不同副作用拆成多个 useEffect,每个只关心自己的一小块逻辑,而不是在一个生命周期函数里塞很多 if/else。

结合一下面试里的常见问法,可以列出几个高频考点,并给出更清晰的答题思路。

答题思路:

  • 先区分 挂载时请求一次依赖变化时重复请求
  • 再分别给出 class 和 Hooks 的写法。

可以整理成这样:

  • 挂载时请求一次:
    • class:在 componentDidMount 里发请求。
    • Hooks:useEffect(() => { fetchData(); }, [])
  • 依赖变化时请求:
    • class:在 componentDidUpdate 里对比 prevProps / prevState,当某些字段变化时再请求。
    • Hooks:useEffect(() => { fetchData(); }, [someKey])

顺带可以提一句:不要在渲染过程中直接发请求(例如在函数组件体或 render 里),容易造成重复调用或无法控制时机。

这里本质考的是:你有没有意识到副作用需要成对出现——订阅 + 取消订阅

  • class 写法:
    • 订阅:componentDidMount
    • 清理:componentWillUnmount
  • Hooks 写法:
    • 订阅:useEffect 副作用部分
    • 清理:useEffect 返回的函数

答题时可以用一个简短例子描述逻辑,而不需要写完整代码:

  • 「挂载时在 componentDidMount 里绑定事件,卸载前在 componentWillUnmount 里解除绑定。」
  • 「用 Hooks 的话,在 useEffect 里绑定,在返回的清理函数里解绑。」

这类问题更多是在考 Hooks 的心智模型,可以从三个点来讲:

  • 依赖数组控制的是 什么时候重新执行副作用
    • []:只在挂载时执行一次。
    • [a, b]:当 ab 变化时执行。
    • 省略依赖:每次渲染后都会执行,通常不是你想要的。
  • 常见坑:
    • 忘记把用到的变量写进依赖数组,导致副作用用的是旧值。
    • 把每次 render 都会变化的对象/函数直接放进依赖,导致无限循环。
  • 面试里如果聊到这一块,可以顺带提一下:
    • 实际项目中可以用 useCallbackuseMemo 等手段稳定依赖,避免不必要的副作用触发。

有时候面试不会直接问「某个生命周期叫什么」,而是给一个场景,考你能不能把生命周期和副作用管理串起来。

常见的几种「陷阱」包括:

  • 在错误的时机操作 DOM
    • 比如在 constructor / 函数组件体里访问 DOM,这时候 DOM 还没挂载。
  • 没有清理副作用导致内存泄漏
    • 事件监听、定时器、订阅等只绑定不解绑,组件多次挂载卸载后问题会堆积。
  • 在更新逻辑里没有条件判断造成死循环
    • componentDidUpdateuseEffect 里不加条件地 setState,导致无限 re-render。

答题建议:

  • 可以先用一句话给出「正确心智模型」:
    • 例如:「我会把组件当成挂载 → 更新 → 卸载三个阶段,在合适的阶段做初始化、同步和清理。」
  • 然后给一个简短示例,说明你在实际项目中是怎么用生命周期解决类似问题的。

把这一整块内容压缩成几句面试时能快速说出来的话,大致可以是这样几个点:

  • 生命周期本质上就是:在挂载、更新、卸载这三个阶段的关键节点上,让你插入初始化、副作用、清理等逻辑。
  • class 时代用一组命名生命周期方法来表达这些时间点,Hooks 时代则用 useEffect 等钩子,把副作用拆成更细的颗粒。
  • 真正在一面里拉开差距的地方,不是能不能背出名字,而是能不能:
    • 把副作用放到合适的阶段;
    • 控制好触发频率;
    • 在合适的时候做清理,避免泄漏和重复执行。

只要沿着「组件所处的阶段」来思考,而不是死记 API 名字,React 生命周期相关的问题通常都能有条理地讲清楚。