说说 React 生命周期有哪些不同阶段?每个阶段对应的方法是?

一、是什么

React 组件从创建到销毁,会经历一系列特定的阶段,每个阶段都有对应的方法可以让开发者在合适的时机执行代码。这套机制就是组件的生命周期(Lifecycle)。

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

  • 挂载阶段(Mounting):组件实例被创建并插入 DOM
  • 更新阶段(Updating):组件的 props 或 state 发生变化,触发重新渲染
  • 卸载阶段(Unmounting):组件从 DOM 中移除

需要注意的是,生命周期方法主要是类组件的概念。函数组件通过 Hooks(主要是 useEffect)实现等价的功能,且更加灵活。

二、类组件的生命周期

2.1 挂载阶段

组件首次渲染时,按以下顺序执行:

constructor()
  → static getDerivedStateFromProps()
  → render()
  → componentDidMount()

constructor(props)

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.handleClick = this.handleClick.bind(this);
  }
}

构造函数是初始化 state 和绑定方法的地方。使用 class fields 语法后,很多场景可以省略 constructor:

class Counter extends React.Component {
  state = { count: 0 };
  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };
}

static getDerivedStateFromProps(props, state)

class ScrollTracker extends React.Component {
  state = { prevScrollTop: 0, isScrollingDown: false };

  static getDerivedStateFromProps(props, state) {
    if (props.scrollTop !== state.prevScrollTop) {
      return {
        prevScrollTop: props.scrollTop,
        isScrollingDown: props.scrollTop > state.prevScrollTop,
      };
    }
    return null;
  }
}

这是一个静态方法,在每次渲染前调用(挂载和更新都会调用)。它接收新的 props 和当前 state,返回一个对象来更新 state,或返回 null 表示不需要更新。使用场景很少,当 state 需要派生自 props 时才需要。

render()

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>{this.props.title}</h1>
        {this.state.showContent && <Content />}
      </div>
    );
  }
}

render 是类组件中唯一必需的方法。它应该是纯函数,不能在其中调用 setState 或执行副作用。

componentDidMount()

class UserList extends React.Component {
  state = { users: [], loading: true };

  componentDidMount() {
    fetch("/api/users")
      .then((res) => res.json())
      .then((users) => this.setState({ users, loading: false }));
  }

  render() {
    if (this.state.loading) return <p>加载中...</p>;
    return (
      <ul>
        {this.state.users.map((u) => (
          <li key={u.id}>{u.name}</li>
        ))}
      </ul>
    );
  }
}

组件挂载到 DOM 后调用,适合执行:数据请求、DOM 操作、订阅事件、启动定时器。

2.2 更新阶段

当 props 或 state 变化时,按以下顺序执行:

static getDerivedStateFromProps()
  → shouldComponentUpdate()
  → render()
  → getSnapshotBeforeUpdate()
  → componentDidUpdate()

shouldComponentUpdate(nextProps, nextState)

class ExpensiveList extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.items !== this.props.items;
  }
  render() { /* ... */ }
}

返回 false 可以跳过本次渲染。React.PureComponent 自动实现了 props 和 state 的浅比较。在函数组件中,React.memo 提供类似的功能。

getSnapshotBeforeUpdate(prevProps, prevState)

class ChatMessages extends React.Component {
  listRef = React.createRef();

  getSnapshotBeforeUpdate(prevProps, prevState) {
    if (prevProps.messages.length < this.props.messages.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef} style={{ overflow: "auto", maxHeight: 300 }}>
        {this.props.messages.map((msg) => (
          <p key={msg.id}>{msg.text}</p>
        ))}
      </div>
    );
  }
}

在 DOM 更新前调用,返回值会传给 componentDidUpdate 的第三个参数。典型使用场景是在消息列表新增消息时保持滚动位置。

componentDidUpdate(prevProps, prevState, snapshot)

class SearchResults extends React.Component {
  componentDidUpdate(prevProps) {
    if (prevProps.query !== this.props.query) {
      this.fetchResults(this.props.query);
    }
  }
}

组件更新后调用。可以在这里根据 props 变化执行副作用,但必须加条件判断,否则会导致无限循环。

2.3 卸载阶段

componentWillUnmount()

class Timer extends React.Component {
  componentDidMount() {
    this.timerID = setInterval(() => this.tick(), 1000);
  }
  componentWillUnmount() {
    clearInterval(this.timerID);
  }
  tick() { this.setState({ time: new Date() }); }
  render() { return <p>{this.state?.time?.toLocaleTimeString()}</p>; }
}

组件卸载前调用,用于清理副作用:取消定时器、取消网络请求、移除事件监听、取消订阅。

三、已废弃的生命周期方法

React 16.3 废弃了三个生命周期方法,在 React 17+ 中需要加 UNSAFE_ 前缀才能使用:

废弃方法UNSAFE 版本废弃原因
componentWillMountUNSAFE_componentWillMount与 SSR 和并发模式不兼容
componentWillReceivePropsUNSAFE_componentWillReceiveProps容易产生副作用 bug
componentWillUpdateUNSAFE_componentWillUpdate与并发模式不兼容

这些方法在 Fiber 架构的并发模式下可能被多次调用,导致不可预期的行为。替代方案:

  • componentWillMount → 使用 constructor + componentDidMount
  • componentWillReceiveProps → 使用 getDerivedStateFromProps
  • componentWillUpdate → 使用 getSnapshotBeforeUpdate

四、函数组件的生命周期等价方案

函数组件没有生命周期方法,但可以通过 Hooks 实现等价功能:

4.1 useEffect 对应关系

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

  // componentDidMount + componentDidUpdate(当 userId 变化时)
  useEffect(() => {
    let cancelled = false;

    async function fetchUser() {
      const res = await fetch(`/api/users/${userId}`);
      const data = await res.json();
      if (!cancelled) {
        setUser(data);
      }
    }

    fetchUser();

    // componentWillUnmount(清理函数)
    return () => {
      cancelled = true;
    };
  }, [userId]);

  if (!user) return <p>加载中...</p>;
  return <h1>{user.name}</h1>;
}

4.2 完整对照表

// 1. constructor → 函数体 + useState 初始化
function Component() {
  const [state, setState] = useState(initialValue);
  // 函数体就是 "constructor"

  // 2. componentDidMount → useEffect(fn, [])
  useEffect(() => {
    // 挂载后执行
    return () => {
      // 3. componentWillUnmount → 清理函数
    };
  }, []);

  // 4. componentDidUpdate → useEffect(fn, [deps])
  useEffect(() => {
    // deps 变化后执行
  }, [someProp, someState]);

  // 5. shouldComponentUpdate → React.memo
  // 包裹在组件外部

  // 6. getDerivedStateFromProps → 渲染期间直接计算
  const derived = computeDerived(props);

  // 7. getSnapshotBeforeUpdate → useLayoutEffect(部分替代)
  useLayoutEffect(() => {
    // DOM 更新后同步执行
  });

  // 8. render → return
  return <div>{state}</div>;
}

// shouldComponentUpdate 的等价
const MemoComponent = React.memo(Component, (prevProps, nextProps) => {
  return prevProps.id === nextProps.id;
});

4.3 useEffect vs useLayoutEffect

Hook执行时机适用场景
useEffect浏览器绘制后异步执行数据请求、事件订阅、日志
useLayoutEffectDOM 更新后、绘制前同步执行读取 DOM 布局、同步调整位置

useLayoutEffect 适合需要在浏览器绘制前同步读取或修改 DOM 布局的场景(如 Tooltip 定位),其他情况一律使用 useEffect

五、React 18 严格模式的影响

React 18 的 StrictMode 在开发环境下会故意双重调用以下函数,以帮助发现副作用问题:

// React 18 开发模式下的行为
function App() {
  // 会被调用两次
  console.log("render");

  // 整个 effect 会执行:mount → unmount → mount
  useEffect(() => {
    console.log("mount");
    return () => console.log("unmount");
  }, []);

  return <div>App</div>;
}

// 控制台输出(开发模式):
// render
// render
// mount
// unmount
// mount

这种双重调用是为了验证组件的副作用是否可以安全地被清理和重新创建,这对 React 的未来特性(如 Offscreen API)至关重要。在生产环境中不会双重调用。

类组件中受影响的方法包括:constructorrendergetDerivedStateFromPropsshouldComponentUpdate

六、总结

类组件:生命周期方法提供了在组件各个阶段介入的精确钩子,但也带来了逻辑分散(相关逻辑分散在不同生命周期中)和代码复用困难(只能通过 HOC 或 render props)等问题。

函数组件 + HooksuseEffect 以"副作用"为中心组织代码,将相关逻辑聚合在一起,通过自定义 Hook 实现逻辑复用。这是 React 当前推荐的模式。

面试建议:重点掌握函数组件的 Hooks 用法,类组件的生命周期作为理解旧代码的知识储备。理解 React 18 严格模式的双重调用机制,以及各个 Hook 的执行时序。