09-React组件生命周期

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)
    • 六、怎么用:类组件生命周期方法详解
    • 七、企业项目实战:生命周期在生产环境中的应用
      • [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 组件生命周期提供了一套精确的时间钩子,让你能够在组件从创建到销毁的每个关键节点插入自定义逻辑。掌握生命周期,就是掌握了组件行为的控制权。

核心解决思路可以概括为三句话:

  1. 挂载时初始化 :在 componentDidMount 中做一次性的副作用操作(请求数据、建立连接、绑定事件)。
  2. 更新时同步 :在 componentDidUpdate 中监听 props/state 变化,做增量更新(重新请求、重置状态),但必须加条件判断。
  3. 卸载时清理 :在 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(已废弃)
  • componentDidMount
  • componentWillReceiveProps(已废弃)
  • shouldComponentUpdate
  • componentWillUpdate(已废弃)
  • componentDidUpdate
  • componentWillUnmount

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)

挂载阶段按以下顺序执行:

  1. constructor
  2. static getDerivedStateFromProps
  3. render
  4. componentDidMount
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 变化时,组件进入更新阶段,按以下顺序执行:

  1. static getDerivedStateFromProps
  2. shouldComponentUpdate
  3. render
  4. getSnapshotBeforeUpdate
  5. componentDidUpdate
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 中当 channel prop 变化时,先断开旧连接再建立新连接。
  • 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 有什么区别?

参考答案:

  1. 执行时机不同 :componentDidMount 在组件挂载后同步执行一次;useEffect 默认在每次渲染后异步执行,通过依赖数组控制执行时机。
  2. 覆盖范围不同 :componentDidMount 只覆盖挂载阶段;一个 useEffect 可以同时覆盖挂载、更新和卸载(通过清理函数)。
  3. 闭包特性 :useEffect 中的函数会捕获当前渲染的 props 和 state,存在闭包陷阱;类组件中通过 this.props 始终获取最新值。
  4. 严格模式行为 :React 18 严格模式下,useEffect 在开发环境会执行两次(挂载-卸载-挂载),帮助发现清理逻辑问题;componentDidMount 也会有类似的双重调用行为。

模拟 componentDidMount 的写法是 useEffect(fn, []),但严格来说二者并不完全等价。

面试题 3:为什么 componentWillMount、componentWillReceiveProps、componentWillUpdate 被废弃了?

参考答案:

这三个方法被标记为 UNSAFE_ 前缀并逐步废弃,主要原因是它们在 React 引入异步渲染(Concurrent Rendering)后存在安全隐患:

  1. componentWillMount:在服务端渲染和客户端都会调用,容易导致逻辑重复;在此方法中发起请求会浪费一次渲染周期。
  2. componentWillReceiveProps :只在父组件重新渲染时触发,组件自身 setState 不会触发,语义不清晰;容易被误用为"props 变化时更新 state",导致派生 state 问题。
  3. 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 团队有意设计的"压力测试"机制,目的是帮助开发者发现以下问题:

  1. 忘记在 componentWillUnmount 中清理副作用。
  2. 清理逻辑不完整,与挂载逻辑不对称。
  3. 副作用不是幂等的,多次执行会产生错误结果。

这种双重调用只在开发环境发生,生产环境不会有此行为。

面试题 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 之后这个警告被移除了,但仍然不建议这样做,因为它表示存在内存泄漏------异步操作在组件卸载后仍在执行。

避免方法:

  1. 在 componentWillUnmount 中取消未完成的请求(如使用 AbortController)。
  2. 设置一个 isMounted 标记,在异步回调中检查标记后再 setState。
  3. 使用 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 中执行副作用?

参考答案:

  1. 纯函数要求 :React 要求 render 是纯函数,相同的 props 和 state 应该返回相同的输出。副作用会破坏这一保证。
  2. 调用次数不确定 :在并发渲染模式下,render 可能被调用多次甚至被中断和放弃,如果在 render 中执行副作用(如发起请求),会导致重复请求或不可预测的行为。
  3. DOM 未就绪 :render 执行时 DOM 还没有更新,此时操作 DOM 读取到的是旧状态。
  4. 性能问题:每次渲染都执行副作用会严重影响性能。

副作用应该放在 componentDidMount、componentDidUpdate 或 useEffect 中执行。

十一、总结

React 组件生命周期是前端开发中必须掌握的核心概念。从类组件的七个生命周期方法,到 Hooks 时代的 useEffect,虽然 API 形式在变,但核心思想始终如一:在正确的时间做正确的事,并在结束时清理干净。

记住三个黄金法则:

  1. 挂载时做初始化,卸载时做清理,二者必须对称。
  2. 更新时必须加条件判断,比较 prevProps/prevState,避免无限循环。
  3. render 保持纯函数,副作用一律放在生命周期方法或 useEffect 中。

掌握了生命周期,你就掌握了 React 组件行为的控制权,那些曾经让你头疼的内存泄漏、无限渲染、数据陈旧等问题,都将迎刃而解。


本文为原创文章,如需转载,请联系作者获得授权,并注明出处。

相关推荐
IT_陈寒1 小时前
SpringBoot开发实战:从入门到精通
前端·人工智能·后端
乘风gg1 小时前
一个人一个月合并 2500 PRs 的秘密是什么?
前端·ai编程·claude
郑州光合科技余经理1 小时前
本地生活服务系统:预约状态和进度通知接口怎么拆
java·开发语言·前端·javascript·后端·php·生活
Peter-Code2 小时前
Nginx 常用操作整理(前端)
服务器·前端·nginx
Java后端的Ai之路11 小时前
06-React-Props
前端·react.js·前端框架
Flynt12 小时前
开源"间谍卫星"说数据全是真的,我把它的免key数据源挨个试了一遍
前端·开源·数据可视化
Csvn12 小时前
渲染管线
前端
程序员大阳13 小时前
副队长CSS教程(33)–圆角与透明
前端·css·透明·圆角
志尊宝13 小时前
Vue3 零基础每日笔记(082):文件上传实战——el-upload 校验、自定义上传、头像预览
前端·javascript·vue.js·笔记·#javascript·#vue.js