
09、React 组件生命周期
作为一名在前端领域摸爬滚打多年的开发者,我见过太多人因为不理解 React 组件生命周期而在项目中踩坑。定时器忘了清除导致内存泄漏,componentDidUpdate 里直接 setState 引发无限渲染,网络请求在 props 变化时没有重新触发导致数据陈旧------这些问题的根源,都是对组件生命周期的一知半解。
本文将从真实痛点出发,系统拆解 React 组件生命周期的前世今生,覆盖类组件与 Hooks 两套体系,结合企业级项目实战代码,让你从"知道有这些方法"进阶到"知道为什么这么用"。
文章目录
- [09、React 组件生命周期](#09、React 组件生命周期)
-
- 一、痛点场景:那些年我们踩过的生命周期坑
-
- [1.1 内存泄漏:定时器忘了清除](#1.1 内存泄漏:定时器忘了清除)
- [1.2 无限循环:componentDidUpdate 中无脑 setState](#1.2 无限循环:componentDidUpdate 中无脑 setState)
- [1.3 数据陈旧:props 变化时没有重新请求](#1.3 数据陈旧:props 变化时没有重新请求)
- [1.4 闪烁问题:DOM 操作时机不对](#1.4 闪烁问题:DOM 操作时机不对)
- [1.5 性能浪费:不必要的重新渲染](#1.5 性能浪费:不必要的重新渲染)
- 二、痛点解决方案:生命周期思维的核心价值
- [三、是什么:React 组件生命周期的本质](#三、是什么:React 组件生命周期的本质)
-
- [3.1 专业解释](#3.1 专业解释)
- [3.2 大白话](#3.2 大白话)
- [3.3 生活案例](#3.3 生活案例)
- 四、为什么用:掌握生命周期的核心价值
-
- [4.1 精确控制副作用时机](#4.1 精确控制副作用时机)
- [4.2 资源管理与内存安全](#4.2 资源管理与内存安全)
- [4.3 性能优化的抓手](#4.3 性能优化的抓手)
- [4.4 与第三方库集成的桥梁](#4.4 与第三方库集成的桥梁)
- [五、演进历史:从 createClass 到 Hooks 的变革之路](#五、演进历史:从 createClass 到 Hooks 的变革之路)
-
- [5.1 上古时代:React.createClass(React 0.13 之前)](#5.1 上古时代:React.createClass(React 0.13 之前))
- [5.2 类组件时代:ES6 Class(React 0.13 - 16.2)](#5.2 类组件时代:ES6 Class(React 0.13 - 16.2))
- [5.3 生命周期改革:React 16.3](#5.3 生命周期改革:React 16.3)
- [5.4 Hooks 革命:React 16.8](#5.4 Hooks 革命:React 16.8)
- [5.5 并发渲染:React 18](#5.5 并发渲染:React 18)
- 六、怎么用:类组件生命周期方法详解
-
- [6.1 挂载阶段(Mounting)](#6.1 挂载阶段(Mounting))
-
- constructor
- [static getDerivedStateFromProps](#static getDerivedStateFromProps)
- render
- componentDidMount
- [6.2 更新阶段(Updating)](#6.2 更新阶段(Updating))
- [6.3 卸载阶段(Unmounting)](#6.3 卸载阶段(Unmounting))
- [6.4 类组件生命周期与 Hooks 对应关系](#6.4 类组件生命周期与 Hooks 对应关系)
- 七、企业项目实战:生命周期在生产环境中的应用
-
- [7.1 场景一:数据请求与加载状态管理](#7.1 场景一:数据请求与加载状态管理)
- [7.2 场景二:WebSocket 实时通信](#7.2 场景二:WebSocket 实时通信)
- [7.3 场景三:ECharts 图表集成](#7.3 场景三:ECharts 图表集成)
- [7.4 场景四:Hooks 版本的等效实现](#7.4 场景四:Hooks 版本的等效实现)
- [八、竞品对比:React vs Vue vs Angular vs Svelte](#八、竞品对比:React vs Vue vs Angular vs Svelte)
-
- [8.1 各框架生命周期特点分析](#8.1 各框架生命周期特点分析)
- 九、常用场景汇总
- 十、面试官高频面试题
-
- [面试题 1:React 组件生命周期的执行顺序是什么?](#面试题 1:React 组件生命周期的执行顺序是什么?)
- [面试题 2:useEffect 和 componentDidMount 有什么区别?](#面试题 2:useEffect 和 componentDidMount 有什么区别?)
- [面试题 3:为什么 componentWillMount、componentWillReceiveProps、componentWillUpdate 被废弃了?](#面试题 3:为什么 componentWillMount、componentWillReceiveProps、componentWillUpdate 被废弃了?)
- [面试题 4:getSnapshotBeforeUpdate 的使用场景是什么?](#面试题 4:getSnapshotBeforeUpdate 的使用场景是什么?)
- [面试题 5:shouldComponentUpdate 和 PureComponent 有什么区别?](#面试题 5:shouldComponentUpdate 和 PureComponent 有什么区别?)
- [面试题 6:在 componentDidUpdate 中调用 setState 需要注意什么?](#面试题 6:在 componentDidUpdate 中调用 setState 需要注意什么?)
- [面试题 7:React 严格模式下生命周期为什么会调用两次?](#面试题 7:React 严格模式下生命周期为什么会调用两次?)
- [面试题 8:如何在函数组件中实现 getSnapshotBeforeUpdate 的功能?](#面试题 8:如何在函数组件中实现 getSnapshotBeforeUpdate 的功能?)
- [面试题 9:组件卸载后调用 setState 会有什么问题?如何避免?](#面试题 9:组件卸载后调用 setState 会有什么问题?如何避免?)
- [面试题 10:为什么不建议在 render 中执行副作用?](#面试题 10:为什么不建议在 render 中执行副作用?)
- 十一、总结
一、痛点场景:那些年我们踩过的生命周期坑

1.1 内存泄漏:定时器忘了清除
这是新手最容易犯的错误。在 componentDidMount 里设置了一个 setInterval,组件卸载时却没有在 componentWillUnmount 中清除。结果就是组件已经不在页面上了,定时器还在后台疯狂执行,轻则浪费 CPU,重则因为引用了已卸载组件的 state 而抛出警告甚至崩溃。
javascript
// 错误示范:只设置不清除
class Timer extends React.Component {
componentDidMount() {
this.interval = setInterval(() => {
console.log('tick');
}, 1000);
}
// 缺少 componentWillUnmount!
}
1.2 无限循环:componentDidUpdate 中无脑 setState
在 componentDidUpdate 中调用 setState 本身是合法的,但如果没有条件判断,每次更新都会触发再次更新,形成死循环。React 会在控制台抛出 "Maximum update depth exceeded" 的错误,页面直接卡死。
javascript
// 错误示范:无条件 setState 导致无限循环
class BadComponent extends React.Component {
componentDidUpdate() {
// 没有比较 prevProps,每次更新都执行
this.setState({ count: this.state.count + 1 });
}
}
1.3 数据陈旧:props 变化时没有重新请求
组件挂载时在 componentDidMount 中请求了数据,但当父组件传入的 userId 变化时,子组件并不会重新执行 componentDidMount。如果只在 componentDidMount 中发请求,数据就会一直停留在第一次的结果上。正确的做法是同时在 componentDidUpdate 中监听 props 变化并重新请求。
1.4 闪烁问题:DOM 操作时机不对
在 render 阶段读取 DOM 元素的位置信息是不可靠的,因为此时 DOM 还没有更新到屏幕上。需要在 componentDidMount 或 componentDidUpdate 中操作 DOM,对于需要在浏览器绘制前同步执行的操作,则应该使用 getSnapshotBeforeUpdate 或 useLayoutEffect。
1.5 性能浪费:不必要的重新渲染
父组件每次状态变化都会触发所有子组件重新渲染,即使子组件的 props 根本没有改变。对于复杂的列表或图表组件,这会造成明显的性能卡顿。shouldComponentUpdate 或 React.memo 就是用来解决这个问题的。
二、痛点解决方案:生命周期思维的核心价值
上述所有痛点,本质上都是同一个问题:没有在正确的时间做正确的事。
React 组件生命周期提供了一套精确的时间钩子,让你能够在组件从创建到销毁的每个关键节点插入自定义逻辑。掌握生命周期,就是掌握了组件行为的控制权。
核心解决思路可以概括为三句话:
- 挂载时初始化 :在
componentDidMount中做一次性的副作用操作(请求数据、建立连接、绑定事件)。 - 更新时同步 :在
componentDidUpdate中监听 props/state 变化,做增量更新(重新请求、重置状态),但必须加条件判断。 - 卸载时清理 :在
componentWillUnmount中清除所有副作用(清除定时器、断开连接、解绑事件),与挂载操作一一对应。
三、是什么:React 组件生命周期的本质

3.1 专业解释
React 组件生命周期是指组件从被创建、挂载到 DOM、更新渲染,直到被卸载销毁的整个过程中,React 框架按固定顺序自动调用的一系列特殊方法。这些方法为开发者提供了在组件不同阶段执行自定义逻辑的钩子(Hook)。
React 官方将生命周期划分为三个阶段:
- Mounting(挂载阶段):组件实例被创建并插入真实 DOM 的过程。
- Updating(更新阶段):组件的 props 或 state 发生变化,触发重新渲染的过程。
- Unmounting(卸载阶段):组件从 DOM 中被移除的过程。
3.2 大白话
你可以把 React 组件想象成一个演员上台表演。
- 挂载 就是演员化妆、穿好戏服、走上舞台的过程。上台之后(
componentDidMount),他可以开始和观众互动、播放背景音乐。 - 更新 就是演出过程中剧情推进,演员根据剧本变化调整台词和动作。每次变化后(
componentDidUpdate),他都有机会调整状态。 - 卸载 就是演出结束,演员下台。下台前(
componentWillUnmount),他必须把麦克风关掉、把道具收拾好,不然下台了麦克风还开着就会出事故。
生命周期方法就是演员在每个阶段可以做的事情清单,告诉你什么时候该做什么事。
3.3 生活案例
以开一家餐厅为例:
- 挂载阶段 :租店面、装修、招聘员工、采购食材(
componentDidMount中初始化),开业当天正式迎客。 - 更新阶段 :根据客流量变化调整菜单(
componentDidUpdate中响应变化),如果发现某道菜卖得不好就下架(条件判断后更新 state)。 - 卸载阶段 :餐厅关门停业,必须结清员工工资、退租、处理剩余食材(
componentWillUnmount中清理资源),不然人走了房租还在扣就是资源泄漏。
如果餐厅关门了但煤气没关(定时器没清除),就会引发安全事故------这和 React 中的内存泄漏是一个道理。
四、为什么用:掌握生命周期的核心价值
4.1 精确控制副作用时机
React 的渲染过程是纯函数式的,render 方法中不应该执行任何副作用(网络请求、DOM 操作、订阅事件)。生命周期方法提供了副作用的安全执行窗口,确保操作在 DOM 就绪后执行,避免读取到未更新的 DOM 状态。
4.2 资源管理与内存安全
在单页应用中,组件的挂载和卸载是高频操作。如果不在卸载时清理资源,每切换一次页面就泄漏一批定时器、事件监听器和网络连接,应用运行越久越卡,最终崩溃。生命周期的"挂载-卸载"对称设计,强制开发者建立"申请-释放"的资源管理意识。
4.3 性能优化的抓手
shouldComponentUpdate 和 getDerivedStateFromProps 等方法让你能够精细控制组件的渲染行为。在大型企业应用中,一个列表组件减少不必要的重渲染,可能带来数百毫秒的性能提升。
4.4 与第三方库集成的桥梁
很多第三方库(图表库、地图库、富文本编辑器)需要直接操作 DOM。生命周期方法告诉你什么时候 DOM 已经准备好可以初始化第三方库,什么时候需要在数据变化时更新库实例,什么时候需要销毁库实例释放资源。
五、演进历史:从 createClass 到 Hooks 的变革之路

5.1 上古时代:React.createClass(React 0.13 之前)
最早的 React 使用 React.createClass 来创建组件,通过 getInitialState 初始化状态,getDefaultProps 设置默认 props。生命周期方法已经存在,但写法相对原始,不支持 ES6 类语法。
javascript
// React 0.13 之前的写法
var OldComponent = React.createClass({
getInitialState: function() {
return { count: 0 };
},
componentDidMount: function() {
console.log('mounted');
},
render: function() {
return <div>{this.state.count}</div>;
}
});
5.2 类组件时代:ES6 Class(React 0.13 - 16.2)
React 0.13 引入了 ES6 Class 语法,React.createClass 被弃用。组件通过继承 React.Component 来创建,constructor 取代了 getInitialState。这一时期的生命周期方法包括:
componentWillMount(已废弃)componentDidMountcomponentWillReceiveProps(已废弃)shouldComponentUpdatecomponentWillUpdate(已废弃)componentDidUpdatecomponentWillUnmount
5.3 生命周期改革:React 16.3
React 16.3 是生命周期发展的重要里程碑。为了支持即将到来的异步渲染(Concurrent Rendering),React 团队废弃了三个不安全的生命周期方法:
componentWillMount→ 被constructor+componentDidMount替代componentWillReceiveProps→ 被static getDerivedStateFromProps替代componentWillUpdate→ 被getSnapshotBeforeUpdate+componentDidUpdate替代
同时引入了两个新方法:
static getDerivedStateFromProps(props, state):在每次渲染前调用,用于根据 props 派生 state。getSnapshotBeforeUpdate(prevProps, prevState):在 DOM 更新前调用,捕获滚动位置等信息。
5.4 Hooks 革命:React 16.8
2019 年 2 月,React 16.8 正式发布 Hooks,这是 React 发展史上最重大的变革之一。Hooks 让你在不编写类的情况下使用 state 和其他 React 特性。
useEffect 这个 Hook 可以覆盖 componentDidMount、componentDidUpdate 和 componentWillUnmount 三个生命周期方法的组合功能:
javascript
import { useState, useEffect } from 'react';
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
// componentDidMount + componentDidUpdate
const interval = setInterval(() => {
setCount(c => c + 1);
}, 1000);
// componentWillUnmount
return () => clearInterval(interval);
}, []); // 空依赖数组 = 只在挂载时执行一次
return <div>{count}</div>;
}
Hooks 的核心理念是"同步副作用"而非"响应生命周期事件",这是一种思维范式的转变。
5.5 并发渲染:React 18
React 18 引入了并发渲染、自动批处理、Transitions 等新特性。生命周期方法在并发模式下的行为更加可预测,useEffect 的执行时机也有了更明确的语义。React 18 还引入了 useEffect 的严格模式双重调用,帮助开发者发现清理逻辑中的 bug。
六、怎么用:类组件生命周期方法详解
6.1 挂载阶段(Mounting)
挂载阶段按以下顺序执行:
constructorstatic getDerivedStateFromPropsrendercomponentDidMount
constructor
构造函数,在组件挂载前调用。用于初始化 state 和绑定事件处理函数。
javascript
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.handleClick = this.handleClick.bind(this);
}
}
注意:在 constructor 中不要调用 setState,直接赋值 this.state 即可;也不要执行副作用操作。
static getDerivedStateFromProps
这是一个静态方法,在每次渲染前调用,包括初始挂载和后续更新。它接收当前的 props 和 state,返回一个对象来更新 state,或者返回 null 表示不需要更新。
javascript
class EmailForm extends React.Component {
state = {
email: '',
prevUserId: null
};
static getDerivedStateFromProps(props, state) {
// 当 userID 变化时,重置 email 输入框
if (props.userID !== state.prevUserId) {
return {
prevUserId: props.userID,
email: props.defaultEmail
};
}
return null;
}
}
这个方法适用于极少数"state 依赖 props 变化"的场景。React 官方建议优先考虑完全受控组件或使用 key 重置组件,而不是滥用这个方法。
render
render 是类组件中唯一必须实现的方法。它应该是一个纯函数,根据 props 和 state 返回要渲染的 UI。在 render 中不要执行副作用、不要直接修改 state、不要操作 DOM。
javascript
render() {
return (
<div className="user-card">
<h2>{this.props.name}</h2>
<p>{this.state.bio}</p>
</div>
);
}
componentDidMount
组件挂载到 DOM 后立即调用,且只调用一次。这是执行副作用的最佳时机:发起网络请求、建立 WebSocket 连接、设置定时器、操作 DOM、初始化第三方库。
javascript
class UserProfile extends React.Component {
state = { user: null, loading: true };
componentDidMount() {
// 发起网络请求
fetch(`/api/users/${this.props.userId}`)
.then(res => res.json())
.then(data => this.setState({ user: data, loading: false }));
// 绑定全局事件
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
// 与 componentDidMount 对称,清理资源
window.removeEventListener('resize', this.handleResize);
}
}
6.2 更新阶段(Updating)
当 props 或 state 变化时,组件进入更新阶段,按以下顺序执行:
static getDerivedStateFromPropsshouldComponentUpdaterendergetSnapshotBeforeUpdatecomponentDidUpdate
shouldComponentUpdate
在接收新的 props 或 state 时、渲染前调用。返回 true(默认)表示继续渲染,返回 false 表示跳过本次渲染。这是性能优化的重要手段。
javascript
class ListItem extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
// 只有当内容真正变化时才重新渲染
return nextProps.item.id !== this.props.item.id ||
nextProps.item.text !== this.props.item.text;
}
}
更推荐的做法是继承 React.PureComponent,它会自动对 props 和 state 做浅比较,相当于内置了 shouldComponentUpdate。
getSnapshotBeforeUpdate
在最近一次渲染输出提交到 DOM 之前调用。它使组件能够在 DOM 变更前捕获一些信息(如滚动位置),返回的值将作为第三个参数传递给 componentDidUpdate。
经典使用场景是聊天列表:新消息到来时,保持用户的滚动位置不跳动。
javascript
class ChatList extends React.Component {
listRef = React.createRef();
getSnapshotBeforeUpdate(prevProps, prevState) {
// 在 DOM 更新前,记录是否滚动到底部
const list = this.listRef.current;
const isAtBottom = list.scrollTop + list.clientHeight >= list.scrollHeight - 10;
return isAtBottom ? list.scrollHeight : null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 如果之前在底部,更新后自动滚动到底部
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return <div ref={this.listRef}>{this.props.messages.map(...)}</div>;
}
}
componentDidUpdate
组件更新后立即调用,首次渲染不会调用。可以在这里操作 DOM、发起网络请求,但必须比较 prevProps/prevState 来避免无限循环。
javascript
class UserPosts extends React.Component {
state = { posts: [] };
componentDidMount() {
this.fetchPosts(this.props.userId);
}
componentDidUpdate(prevProps) {
// 关键:必须比较 props 是否变化
if (this.props.userId !== prevProps.userId) {
this.fetchPosts(this.props.userId);
}
}
fetchPosts(userId) {
fetch(`/api/users/${userId}/posts`)
.then(res => res.json())
.then(posts => this.setState({ posts }));
}
}
6.3 卸载阶段(Unmounting)
componentWillUnmount
组件从 DOM 中移除前调用。用于清理所有在 componentDidMount 中创建的资源:清除定时器、取消网络请求、断开 WebSocket、移除事件监听器、销毁第三方库实例。
javascript
class RealTimeDashboard extends React.Component {
componentDidMount() {
this.interval = setInterval(this.fetchData, 5000);
this.socket = new WebSocket('wss://api.example.com/realtime');
window.addEventListener('resize', this.handleResize);
}
componentWillUnmount() {
// 一一对应,全部清理
clearInterval(this.interval);
this.socket.close();
window.removeEventListener('resize', this.handleResize);
}
}
6.4 类组件生命周期与 Hooks 对应关系

| 类组件生命周期方法 | Hooks 实现方式 | 说明 |
|---|---|---|
constructor |
函数组件顶层代码 + useState 初始值 |
初始化状态和变量 |
componentDidMount |
useEffect(fn, []) |
空依赖数组,挂载后执行一次 |
componentDidUpdate |
useEffect(fn, [deps]) |
依赖变化时执行 |
componentWillUnmount |
useEffect 返回的清理函数 |
卸载前执行清理 |
shouldComponentUpdate |
React.memo |
浅比较 props 跳过渲染 |
getDerivedStateFromProps |
渲染期间调用 setState |
极罕见场景 |
getSnapshotBeforeUpdate |
useLayoutEffect + useRef |
DOM 更新前捕获信息 |
需要强调的是,useEffect 并不是生命周期方法的简单替代品。它的设计理念是"同步副作用与状态",而不是"响应生命周期事件"。一个 useEffect 可以同时覆盖挂载、更新和卸载三个阶段,这取决于依赖数组和清理函数的组合。
七、企业项目实战:生命周期在生产环境中的应用

7.1 场景一:数据请求与加载状态管理
在企业级后台管理系统中,列表页的数据加载是最常见的需求。需要处理加载中、加载成功、加载失败三种状态,并且在查询条件变化时重新请求。
javascript
import React from 'react';
import { Spin, Table, message } from 'antd';
class OrderList extends React.Component {
state = {
data: [],
loading: false,
pagination: { current: 1, pageSize: 20, total: 0 }
};
componentDidMount() {
this.fetchData();
}
componentDidUpdate(prevProps, prevState) {
// 监听查询条件和分页变化
const { query } = this.props;
const { pagination } = this.state;
if (
JSON.stringify(query) !== JSON.stringify(prevProps.query) ||
pagination.current !== prevState.pagination.current ||
pagination.pageSize !== prevState.pagination.pageSize
) {
this.fetchData();
}
}
componentWillUnmount() {
this.canceled = true;
}
fetchData = async () => {
this.setState({ loading: true });
try {
const { query } = this.props;
const { pagination } = this.state;
const res = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...query, ...pagination })
});
const result = await res.json();
if (!this.canceled) {
this.setState({
data: result.list,
loading: false,
pagination: { ...pagination, total: result.total }
});
}
} catch (error) {
if (!this.canceled) {
this.setState({ loading: false });
message.error('数据加载失败');
}
}
};
render() {
const { data, loading, pagination } = this.state;
return (
<Spin spinning={loading}>
<Table
dataSource={data}
pagination={pagination}
onChange={(pag) => this.setState({ pagination: pag })}
columns={[
{ title: '订单号', dataIndex: 'orderNo' },
{ title: '金额', dataIndex: 'amount' },
{ title: '状态', dataIndex: 'status' }
]}
/>
</Spin>
);
}
}
export default OrderList;
关键点:
componentDidMount首次加载数据。componentDidUpdate监听查询条件和分页变化,重新请求。componentWillUnmount中设置canceled标记,防止组件卸载后仍调用setState。- 使用
JSON.stringify比较对象类型的查询条件。
7.2 场景二:WebSocket 实时通信
在实时监控、即时通讯等场景中,WebSocket 连接的建立和断开必须严格跟随组件生命周期。
javascript
import React from 'react';
class RealTimeMonitor extends React.Component {
state = {
connected: false,
messages: []
};
componentDidMount() {
this.connect(this.props.channel);
}
componentDidUpdate(prevProps) {
// 频道变化时,断开旧连接,建立新连接
if (this.props.channel !== prevProps.channel) {
this.disconnect();
this.connect(this.props.channel);
}
}
componentWillUnmount() {
this.disconnect();
}
connect = (channel) => {
this.ws = new WebSocket(`wss://api.example.com/monitor/${channel}`);
this.ws.onopen = () => {
this.setState({ connected: true });
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.setState(prev => ({
messages: [...prev.messages.slice(-99), data]
}));
};
this.ws.onclose = () => {
this.setState({ connected: false });
};
this.ws.onerror = () => {
// 自动重连逻辑
setTimeout(() => {
if (this.ws && this.ws.readyState === WebSocket.CLOSED) {
this.connect(channel);
}
}, 3000);
};
};
disconnect = () => {
if (this.ws) {
this.ws.onclose = null; // 防止触发重连
this.ws.close();
this.ws = null;
}
};
render() {
const { connected, messages } = this.state;
return (
<div className="monitor">
<div className={`status ${connected ? 'online' : 'offline'}`}>
{connected ? '已连接' : '连接中...'}
</div>
<div className="message-list">
{messages.map((msg, i) => (
<div key={i} className="message">{msg.content}</div>
))}
</div>
</div>
);
}
}
export default RealTimeMonitor;
关键点:
componentDidMount建立连接。componentDidUpdate中当channelprop 变化时,先断开旧连接再建立新连接。componentWillUnmount断开连接,并将onclose置空防止触发自动重连。- 消息列表保留最近 100 条,防止内存无限增长。
7.3 场景三:ECharts 图表集成
第三方图表库需要直接操作 DOM,生命周期方法是集成的关键。
javascript
import React from 'react';
import * as echarts from 'echarts';
class BarChart extends React.Component {
chartRef = React.createRef();
chartInstance = null;
componentDidMount() {
// DOM 已就绪,初始化图表
this.chartInstance = echarts.init(this.chartRef.current);
this.updateChart(this.props.data);
// 监听窗口大小变化,自适应图表
window.addEventListener('resize', this.handleResize);
}
componentDidUpdate(prevProps) {
// 数据变化时更新图表
if (JSON.stringify(this.props.data) !== JSON.stringify(prevProps.data)) {
this.updateChart(this.props.data);
}
}
componentWillUnmount() {
// 销毁图表实例,释放内存
window.removeEventListener('resize', this.handleResize);
if (this.chartInstance) {
this.chartInstance.dispose();
this.chartInstance = null;
}
}
handleResize = () => {
if (this.chartInstance) {
this.chartInstance.resize();
}
};
updateChart = (data) => {
if (!this.chartInstance) return;
this.chartInstance.setOption({
xAxis: { type: 'category', data: data.labels },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: data.values }],
tooltip: { trigger: 'axis' }
});
};
render() {
return <div ref={this.chartRef} style={{ width: '100%', height: 400 }} />;
}
}
export default BarChart;
关键点:
componentDidMount中初始化 ECharts 实例,因为此时 DOM 元素已经存在。componentDidUpdate中比较数据变化,调用setOption更新图表。componentWillUnmount中调用dispose销毁实例,这是 ECharts 官方推荐的释放资源方式。- 窗口 resize 事件的绑定和解绑对称。
7.4 场景四:Hooks 版本的等效实现
以下是使用 Hooks 实现同样的 ECharts 集成,展示 useEffect 如何替代生命周期三件套:
javascript
import React, { useRef, useEffect } from 'react';
import * as echarts from 'echarts';
function BarChart({ data }) {
const chartRef = useRef(null);
const instanceRef = useRef(null);
// 等效于 componentDidMount + componentWillUnmount
useEffect(() => {
instanceRef.current = echarts.init(chartRef.current);
const handleResize = () => instanceRef.current?.resize();
window.addEventListener('resize', handleResize);
// 清理函数 = componentWillUnmount
return () => {
window.removeEventListener('resize', handleResize);
instanceRef.current?.dispose();
instanceRef.current = null;
};
}, []); // 空依赖,只执行一次
// 等效于 componentDidUpdate,监听 data 变化
useEffect(() => {
if (!instanceRef.current) return;
instanceRef.current.setOption({
xAxis: { type: 'category', data: data.labels },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: data.values }],
tooltip: { trigger: 'axis' }
});
}, [data]); // data 变化时执行
return <div ref={chartRef} style={{ width: '100%', height: 400 }} />;
}
export default BarChart;
可以看到,Hooks 版本将不同关注点的逻辑拆分到不同的 useEffect 中,每个 effect 只负责一件事,代码更加清晰。
八、竞品对比:React vs Vue vs Angular vs Svelte
不同前端框架对组件生命周期的设计理念各不相同。以下是四大主流框架的生命周期机制对比:
| 对比维度 | React(类组件) | React(Hooks) | Vue 3(组合式) | Angular | Svelte 5 |
|---|---|---|---|---|---|
| 设计理念 | 类继承方法重写 | 副作用同步 | 生命周期钩子函数 | 装饰器+接口实现 | 运行时效果(runes) |
| 挂载后 | componentDidMount |
useEffect(fn, []) |
onMounted |
ngAfterViewInit |
$effect |
| 更新后 | componentDidUpdate |
useEffect(fn, [deps]) |
onUpdated |
ngOnChanges / ngAfterViewChecked |
$effect(自动追踪依赖) |
| 卸载前 | componentWillUnmount |
useEffect 返回清理函数 |
onBeforeUnmount |
ngOnDestroy |
$effect 清理函数 |
| props 变化 | getDerivedStateFromProps / componentDidUpdate |
useEffect(fn, [prop]) |
watch / watchEffect |
ngOnChanges |
$effect(自动追踪) |
| 渲染优化 | shouldComponentUpdate / PureComponent |
React.memo / useMemo |
内置响应式依赖追踪 | OnPush 变更检测策略 |
编译器自动优化 |
| 学习曲线 | 中等(需理解 this 绑定) | 较陡(需理解闭包和依赖数组) | 平缓(直观的钩子命名) | 陡峭(装饰器、依赖注入、Zone.js) | 平缓(编译时处理) |
| 心智模型 | 生命周期事件驱动 | 状态同步驱动 | 响应式依赖追踪 | 变更检测循环 | 细粒度响应式 |
| 典型代码量 | 较多(类结构+方法) | 较少(函数式) | 较少 | 最多(装饰器+接口) | 最少 |
| 企业级生态 | 极成熟 | 极成熟 | 成熟 | 成熟(企业级首选) | 成长中 |
8.1 各框架生命周期特点分析
React 类组件 :生命周期方法命名直观,执行顺序明确,但需要处理 this 绑定问题,逻辑分散在不同方法中容易导致关注点混乱。
React Hooks :useEffect 一个 API 覆盖三个阶段,依赖数组是核心概念。优点是逻辑可以按关注点组织,缺点是闭包陷阱和依赖数组遗漏是新手高频踩坑点。
Vue 3 组合式 API :生命周期钩子命名最直观(onMounted、onUpdated、onUnmounted),配合响应式系统,开发者不需要手动管理依赖数组,watch 和 watchEffect 自动追踪依赖。
Angular :生命周期最丰富(ngOnChanges、ngOnInit、ngDoCheck、ngAfterContentInit、ngAfterViewInit 等),基于 Zone.js 的变更检测机制,适合大型企业级应用,但学习成本最高。
Svelte 5 :引入 runes 概念,$effect 自动追踪依赖,不需要手动指定依赖数组,编译器在构建时完成优化,运行时开销最小。
九、常用场景汇总
| 场景 | 推荐生命周期 / Hook | 关键注意事项 |
|---|---|---|
| 首次数据请求 | componentDidMount / useEffect(fn, []) |
处理 loading 和 error 状态 |
| props 变化重新请求 | componentDidUpdate + 条件判断 / useEffect(fn, [prop]) |
必须比较 prevProps,避免无限循环 |
| 定时器管理 | componentDidMount 设置 + componentWillUnmount 清除 |
必须成对出现 |
| WebSocket 连接 | componentDidMount 连接 + componentDidUpdate 重连 + componentWillUnmount 断开 |
频道变化时先断后连 |
| 事件监听 | componentDidMount 绑定 + componentWillUnmount 解绑 |
注意事件处理函数引用一致 |
| 第三方库初始化 | componentDidMount 初始化 + componentDidUpdate 更新 + componentWillUnmount 销毁 |
DOM 必须已就绪 |
| 滚动位置保持 | getSnapshotBeforeUpdate + componentDidUpdate |
在 DOM 更新前捕获位置 |
| 性能优化 | shouldComponentUpdate / PureComponent / React.memo |
浅比较,避免深比较带来的性能问题 |
| 表单重置 | getDerivedStateFromProps 或 key 属性 |
优先使用 key 重置组件 |
| 动画控制 | componentDidUpdate 中触发 + useLayoutEffect |
需要在绘制前同步执行时用 useLayoutEffect |
十、面试官高频面试题

面试题 1:React 组件生命周期的执行顺序是什么?
参考答案:
挂载阶段:constructor → static getDerivedStateFromProps → render → componentDidMount
更新阶段:static getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate
卸载阶段:componentWillUnmount
需要注意的是,getDerivedStateFromProps 在挂载和更新阶段都会调用,而 componentDidMount 只在挂载时调用一次。
面试题 2:useEffect 和 componentDidMount 有什么区别?
参考答案:
- 执行时机不同 :
componentDidMount在组件挂载后同步执行一次;useEffect默认在每次渲染后异步执行,通过依赖数组控制执行时机。 - 覆盖范围不同 :
componentDidMount只覆盖挂载阶段;一个useEffect可以同时覆盖挂载、更新和卸载(通过清理函数)。 - 闭包特性 :
useEffect中的函数会捕获当前渲染的 props 和 state,存在闭包陷阱;类组件中通过this.props始终获取最新值。 - 严格模式行为 :React 18 严格模式下,
useEffect在开发环境会执行两次(挂载-卸载-挂载),帮助发现清理逻辑问题;componentDidMount也会有类似的双重调用行为。
模拟 componentDidMount 的写法是 useEffect(fn, []),但严格来说二者并不完全等价。
面试题 3:为什么 componentWillMount、componentWillReceiveProps、componentWillUpdate 被废弃了?
参考答案:
这三个方法被标记为 UNSAFE_ 前缀并逐步废弃,主要原因是它们在 React 引入异步渲染(Concurrent Rendering)后存在安全隐患:
- componentWillMount:在服务端渲染和客户端都会调用,容易导致逻辑重复;在此方法中发起请求会浪费一次渲染周期。
- componentWillReceiveProps :只在父组件重新渲染时触发,组件自身
setState不会触发,语义不清晰;容易被误用为"props 变化时更新 state",导致派生 state 问题。 - componentWillUpdate:在此方法中读取 DOM 是不安全的,因为 React 可能在调用此方法和实际更新 DOM 之间执行其他操作。
替代方案分别是:constructor + componentDidMount、static getDerivedStateFromProps、getSnapshotBeforeUpdate + componentDidUpdate。
面试题 4:getSnapshotBeforeUpdate 的使用场景是什么?
参考答案:
getSnapshotBeforeUpdate 在 DOM 更新之前调用,用于捕获一些在 DOM 变更后可能丢失的信息,最典型的场景是保持滚动位置。
例如聊天列表中,当新消息到来导致列表内容增加时,如果用户正在查看历史消息,不应该自动滚动到底部;如果用户已经在底部,则应该自动跟随新消息。通过在 getSnapshotBeforeUpdate 中记录更新前的滚动位置和高度,在 componentDidUpdate 中根据快照调整滚动位置,可以实现无缝的滚动体验。
面试题 5:shouldComponentUpdate 和 PureComponent 有什么区别?
参考答案:
shouldComponentUpdate是手动实现的方法,你可以自定义比较逻辑,返回false跳过渲染。PureComponent是一个基类,它内置了shouldComponentUpdate的实现,对 props 和 state 进行浅比较(shallow comparison)。- 浅比较只比较对象的第一层引用,如果 props 中包含嵌套对象或数组,即使内容变化了,浅比较也可能认为没有变化而跳过渲染。
- 对于函数组件,对应的优化手段是
React.memo,同样是浅比较 props。
注意:shouldComponentUpdate 返回 false 只会跳过当前组件的渲染,不会阻止子组件因自身 state 变化而重新渲染。
面试题 6:在 componentDidUpdate 中调用 setState 需要注意什么?
参考答案:
必须包裹在条件判断中,比较 prevProps 和 this.props(或 prevState 和 this.state),只有在特定值真正变化时才调用 setState。否则会导致无限循环:setState 触发更新 → componentDidUpdate 再次执行 → 再次 setState,最终触发 "Maximum update depth exceeded" 错误。
javascript
componentDidUpdate(prevProps) {
if (this.props.userId !== prevProps.userId) {
this.fetchUserData(this.props.userId);
}
}
面试题 7:React 严格模式下生命周期为什么会调用两次?
参考答案:
React 18 的严格模式(StrictMode)在开发环境下会故意双重调用某些生命周期方法和 useEffect,具体表现为:挂载 → 卸载 → 再次挂载。
这不是 bug,而是 React 团队有意设计的"压力测试"机制,目的是帮助开发者发现以下问题:
- 忘记在
componentWillUnmount中清理副作用。 - 清理逻辑不完整,与挂载逻辑不对称。
- 副作用不是幂等的,多次执行会产生错误结果。
这种双重调用只在开发环境发生,生产环境不会有此行为。
面试题 8:如何在函数组件中实现 getSnapshotBeforeUpdate 的功能?
参考答案:
使用 useLayoutEffect 配合 useRef 可以模拟 getSnapshotBeforeUpdate 的行为。useLayoutEffect 在 DOM 变更后、浏览器绘制前同步执行,可以读取最新的 DOM 状态。
javascript
function ChatList({ messages }) {
const listRef = useRef(null);
const prevHeightRef = useRef(0);
useLayoutEffect(() => {
const list = listRef.current;
// 检查之前是否在底部
const wasAtBottom = list.scrollTop + list.clientHeight >= prevHeightRef.current - 10;
if (wasAtBottom) {
list.scrollTop = list.scrollHeight;
}
prevHeightRef.current = list.scrollHeight;
}, [messages]);
return <div ref={listRef}>{messages.map(...)}</div>;
}
面试题 9:组件卸载后调用 setState 会有什么问题?如何避免?
参考答案:
React 18 之前,组件卸载后调用 setState 会在控制台抛出警告:"Can't perform a React state update on an unmounted component"。React 18 之后这个警告被移除了,但仍然不建议这样做,因为它表示存在内存泄漏------异步操作在组件卸载后仍在执行。
避免方法:
- 在
componentWillUnmount中取消未完成的请求(如使用AbortController)。 - 设置一个
isMounted标记,在异步回调中检查标记后再setState。 - 使用
useEffect的清理函数取消订阅和请求。
javascript
componentDidMount() {
this.controller = new AbortController();
fetch('/api/data', { signal: this.controller.signal })
.then(res => res.json())
.then(data => this.setState({ data }));
}
componentWillUnmount() {
this.controller.abort();
}
面试题 10:为什么不建议在 render 中执行副作用?
参考答案:
- 纯函数要求 :React 要求
render是纯函数,相同的 props 和 state 应该返回相同的输出。副作用会破坏这一保证。 - 调用次数不确定 :在并发渲染模式下,
render可能被调用多次甚至被中断和放弃,如果在render中执行副作用(如发起请求),会导致重复请求或不可预测的行为。 - DOM 未就绪 :
render执行时 DOM 还没有更新,此时操作 DOM 读取到的是旧状态。 - 性能问题:每次渲染都执行副作用会严重影响性能。
副作用应该放在 componentDidMount、componentDidUpdate 或 useEffect 中执行。
十一、总结
React 组件生命周期是前端开发中必须掌握的核心概念。从类组件的七个生命周期方法,到 Hooks 时代的 useEffect,虽然 API 形式在变,但核心思想始终如一:在正确的时间做正确的事,并在结束时清理干净。
记住三个黄金法则:
- 挂载时做初始化,卸载时做清理,二者必须对称。
- 更新时必须加条件判断,比较 prevProps/prevState,避免无限循环。
- render 保持纯函数,副作用一律放在生命周期方法或 useEffect 中。
掌握了生命周期,你就掌握了 React 组件行为的控制权,那些曾经让你头疼的内存泄漏、无限渲染、数据陈旧等问题,都将迎刃而解。
本文为原创文章,如需转载,请联系作者获得授权,并注明出处。