React 生命周期

深入理解 React class component 生命周期:mount → update → unmount 完整流程,以及 Hooks 如何替代生命周期方法。

React 生命周期

历史背景:为什么需要理解生命周期

React 的生命周期概念源自 class component 时代。在 Hooks 出现之前,如果你想让组件在渲染后执行某些操作(比如发起网络请求、订阅事件、操作 DOM),你必须通过生命周期方法来实现。

理解生命周期不仅仅是面试需要——它能帮助你理解 React 渲染模型的本质:React 组件是一个状态到 UI 的映射,而生命周期方法是你在这个映射过程中的各个时间点插入逻辑的钩子

即使你现在只使用 function component 和 Hooks,理解生命周期依然重要,因为:

  1. 你可能在维护使用 class component 的遗留代码
  2. Hooks 的 useEffect 依赖关系本质上是生命周期的另一种表达
  3. 很多 React 概念(Error Boundary、Suspense)的行为与生命周期有内在联系

生命周期阶段总览

React class component 的生命周期分为三个主要阶段:

Mounting(挂载):组件被创建并首次插入 DOM

Updating(更新):组件因 props 或 state 变化而重新渲染

Unmounting(卸载):组件从 DOM 中移除

每个阶段都有对应的生命周期方法,你可以在这些方法中执行特定的逻辑。

flowchart TD
    A[开始] --> B{哪个阶段?}
    B -->|Mounting| C[constructor]
    C --> D[render]
    D --> E[componentDidMount]
    B -->|Updating| F[render]
    F --> G[componentDidUpdate]
    B -->|Unmounting| H[componentWillUnmount]

Mounting 阶段:组件的诞生

constructor

constructor 是组件实例创建时调用的第一个方法。它的主要作用是:

  1. 初始化本地 state
  2. 绑定事件处理函数的 this
class UserCard extends React.Component {
  constructor(props) {
    super(props); // 必须调用 super(props),否则 this.props 在构造函数中会是 undefined

    this.state = {
      isOnline: props.isInitialOnline
    };

    // 绑定 this,否则在回调中 this 会是 undefined
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    console.log('Clicked');
  }

  render() {
    return <div onClick={this.handleClick}>{this.state.isOnline ? 'Online' : 'Offline'}</div>;
  }
}

关于 constructor 的几个要点:

  • 如果你不初始化 state,也不绑定方法,可以不写 constructor(现代 React 实践确实越来越少使用 constructor)
  • super(props) 必须在 this 使用之前调用
  • 不要在 constructor 里调用 setState,直接给 this.state 赋值

render

render 是 class component 唯一必须实现的方法。它返回要渲染的 React 元素(JSX)、数组、Fragment、Portal、字符串、数字、布尔值或 null。

render 应该是纯函数

  • 不修改组件 state
  • 不直接操作 DOM
  • 不调用会在渲染间产生副作用的 API(如 HTTP 请求)
render() {
  // 纯函数:给定相同的 props 和 state,总是返回相同的结果
  return (
    <div className="user-card">
      <h1>{this.props.name}</h1>
      <p>{this.state.isOnline ? 'Online' : 'Offline'}</p>
    </div>
  );
}

componentDidMount

componentDidMount 在组件被渲染并插入 DOM 后立即调用。这个方法是执行副作用的理想时机。

适合在 componentDidMount 中做的事情:

class DataFetcher extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: null,
      loading: true,
      error: null
    };
  }

  componentDidMount() {
    // 发起网络请求 — 适合在 componentDidMount 中
    fetch(this.props.url)
      .then(res => res.json())
      .then(
        data => this.setState({ data, loading: false }),
        error => this.setState({ error, loading: false })
      );

    // 添加 DOM 事件监听 — 适合在 componentDidMount 中
    this.resizeHandler = () => {
      this.setState({ windowWidth: window.innerWidth });
    };
    window.addEventListener('resize', this.resizeHandler);

    // 创建定时器 — 适合在 componentDidMount 中
    this.intervalId = setInterval(() => {
      this.fetchLatestData();
    }, 5000);
  }

  render() {
    if (this.state.loading) return <div>Loading...</div>;
    if (this.state.error) return <div>Error: {this.state.error.message}</div>;
    return <div>{JSON.stringify(this.state.data)}</div>;
  }
}

为什么在这些场景下适合用 componentDidMount:

  • DOM 已经存在,可以安全地操作 DOM 或获取 DOM 尺寸
  • 组件已经挂载,订阅/事件监听已经建立
  • 只执行一次,不需要每次更新都重新执行

Updating 阶段:组件的成长

组件在两种情况下会更新:

  1. props 变化:父组件传递的 props 发生变化
  2. state 变化:组件内部调用 setState 或使用 state hook 变化

shouldComponentUpdate

shouldComponentUpdate 让你控制组件是否需要重新渲染。默认情况下,props 或 state 变化时,React 会重新渲染组件。但你可以通过这个方法告诉 React:「在某些情况下,即使数据变了,也不需要重新渲染」。

class OptimizedList extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // 只有当 list 数据真正变化时才重新渲染
    // 如果是同一个数组引用(即使内容可能变了),也跳过
    return nextProps.list !== this.props.list;
  }

  render() {
    return <ul>{this.props.list.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
  }
}

更现代的做法是使用 React.PureComponent,它默认对 props 和 state 进行浅比较:

class OptimizedList extends React.PureComponent {
  render() {
    return <ul>{this.props.list.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
  }
}

render

每次更新都会调用 render。此时 React 会重新计算虚拟 DOM,diff 算法会决定需要更新哪些真实 DOM。

componentDidUpdate

componentDidUpdate 在 DOM 更新后立即调用。首次渲染不会调用这个方法。

class DataUpdater extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    // 典型用法:根据 props 变化发起请求
    if (this.props.userId !== prevProps.userId) {
      this.fetchUserData(this.props.userId);
    }

    // 或者操作 DOM(此时 DOM 已经更新)
    if (this.props.chartData !== prevProps.chartData) {
      this.chart.update(this.props.chartData);
    }
  }

  render() {
    return <div>Content</div>;
  }
}

componentDidUpdate 的几个要点:

  • 必须包裹在条件语句里(比较 prevProps/prevState),否则会导致无限循环
  • 如果实现 getSnapshotBeforeUpdatecomponentDidUpdate 可以访问 snapshot
  • 可以调用 setState,但必须包裹在条件语句里,否则会导致无限循环

Unmounting 阶段:组件的终结

componentWillUnmount

componentWillUnmount 在组件被卸载前调用,适合清理在 componentDidMount 中创建的副作用:

class SubscriptionManager extends React.Component {
  componentDidMount() {
    this.subscription = dataSource.subscribe(
      data => this.setState({ data })
    );

    this.intervalId = setInterval(() => {
      this.checkForUpdates();
    }, 10000);

    this.resizeHandler = () => console.log('resize');
    window.addEventListener('resize', this.resizeHandler);
  }

  componentWillUnmount() {
    // 取消订阅,防止内存泄漏
    this.subscription.unsubscribe();

    // 清除定时器
    clearInterval(this.intervalId);

    // 移除事件监听
    window.removeEventListener('resize', this.resizeHandler);
  }

  render() {
    return <div>{this.state.data}</div>;
  }
}

componentWillUnmount 的限制:

  • 在此方法中不能调用 setState(组件即将卸载,不应该再触发更新)
  • 如果有异步操作的结果在此之后返回,不应该触发状态更新(需要使用 AbortController 或其他取消机制)

特殊生命周期方法

getDerivedStateFromError

getDerivedStateFromError 是 Error Boundary 的一部分,在子组件抛出错误后调用。它应该返回一个 state 来渲染降级的 UI。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能显示降级 UI
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // 记录错误日志
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

关于 Error Boundary 的要点:

  • 只有 class component 可以成为 Error Boundary
  • Error Boundary 只捕获子组件的错误,不能捕获自身的渲染错误
  • getDerivedStateFromError 用于渲染降级 UI(同步)
  • componentDidCatch 用于记录错误日志(可以包含副作用)

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate 在最近一次渲染输出提交到 DOM 之前调用。它让你的组件在 DOM 变化前获取某些信息(比如滚动位置),这些信息可以在 componentDidUpdate 中使用。

class ScrollingList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
    this.state = {
      messages: []
    };
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 在 DOM 更新前保存滚动位置
    if (prevState.messages.length < this.state.messages.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 在 DOM 更新后恢复滚动位置
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>
        {this.state.messages.map(msg => <div key={msg.id}>{msg.text}</div>)}
      </div>
    );
  }
}

getSnapshotBeforeUpdate 的典型用例:

  • 获取 DOM 元素的大小或位置
  • 保存用户的滚动位置,在更新后恢复
  • 获取 DOM 的某些状态,在 update 后做对比

Hooks 如何替代生命周期

Mount/Update/Unmount 的对应关系

useEffect 是 React 提供的 Hook,用于处理副作用。它在不同场景下的行为可以类比到 class component 的生命周期:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // componentDidMount + componentDidUpdate(当 userId 变化时)
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  // componentDidMount(只在首次渲染时执行)
  useEffect(() => {
    document.title = 'User Profile';

    // componentWillUnmount(cleanup 函数)
    return () => {
      document.title = 'App'; // 清理副作用
    };
  }, []);

  return <div>{user?.name}</div>;
}

精确对应关系

Class Component Hooks 写法 说明
componentDidMount useEffect(() => {...}, []) 空依赖数组表示只在首次渲染后执行
componentDidUpdate useEffect(() => {...}, [dep]) 有依赖项时,在依赖变化后执行
componentWillUnmount useEffect(() => { return () => {...} }, []) cleanup 函数在卸载时执行

useEffect 的执行时机

重要区别: useEffect 的执行时机与 componentDidMount/componentDidUpdate 不同。

componentDidMountcomponentDidUpdate 是在 DOM 更新之后同步执行的,而 useEffect 是在 DOM 更新之后异步执行的(在 React 完成渲染之后,浏览器允许时才执行)。

这意味着:

  • useEffect 不会阻塞浏览器渲染
  • useEffect 中读取 DOM 布局信息,可能会看到布局抖动(因为浏览器已经完成了渲染)
  • useLayoutEffect 才与 class 组件生命周期的同步行为一致
// useEffect:异步,不阻塞渲染
useEffect(() => {
  console.log('Effect runs after paint');
}, []);

// useLayoutEffect:同步,阻塞渲染
useLayoutEffect(() => {
  console.log('Layout effect runs before paint');
}, []);

哪些场景下 Class Component 仍有价值

遗留代码维护

很多现有代码库仍然使用 class component。理解生命周期对于维护和迁移这些代码是必要的。

Error Boundary

目前只有 class component 可以实现 Error Boundary。getDerivedStateFromErrorcomponentDidCatch 都只能在 class component 中使用。

// Error Boundary 必须是 class component
class ErrorBoundary extends React.Component {
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    logError(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Error occurred</h1>;
    }
    return this.props.children;
  }
}

需要复杂生命周期逻辑的场景

某些复杂的生命周期逻辑,用 class component 可能更清晰:

  • 需要在多个生命周期方法间共享逻辑(但现在可以用 custom Hook 解决)
  • getSnapshotBeforeUpdate 的用例(但 useLayoutEffect 可以替代)
  • shouldComponentUpdate 的细粒度控制(但 React.memo 可以替代)

面试中的表达

面试中聊到 React 生命周期,通常是在考察你对 React 渲染模型的深层理解:

React class component 的生命周期分为三个阶段:mount、update、unmount。mount 阶段依次执行 constructor、render、componentDidMount;update 阶段在 props 或 state 变化后执行 render、componentDidUpdate;unmount 阶段执行 componentWillUnmount 做清理。

getDerivedStateFromError 和 getSnapshotBeforeUpdate 是两个特殊的生命周期方法。前者用于 Error Boundary,在子组件出错时渲染降级 UI;后者用于在 DOM 更新前获取快照,比如保存滚动位置。

Hooks 的 useEffect 对应了这些生命周期的组合:空依赖数组的 useEffect 等于 componentDidMount 加 componentWillUnmount;有依赖数组的 useEffect 等于 componentDidMount 加 componentDidUpdate 加 componentWillUnmount。但 useEffect 是异步执行的,不阻塞渲染,这是与 class 组件生命周期的关键区别。如果需要同步执行,应该用 useLayoutEffect。


延展阅读