人人都会AI编程

29.5 类组件生命周期与 Hooks 映射关系

更新时间:2026-07-11

从类组件迁移到函数组件时,最大的困惑之一就是:原来熟悉的生命周期方法(如 componentDidMountcomponentDidUpdatecomponentWillUnmount)在 Hooks 中应该如何表达?理解它们的映射关系,不仅能帮你平稳过渡,还能避免常见的“用 Hooks 模拟生命周期”的思维陷阱。

核心差异:从“生命周期阶段”到“副作用依赖”

类组件的生命周期方法基于“组件在某个阶段做了什么”:

  • 挂载阶段componentDidMount
  • 更新阶段componentDidUpdate
  • 卸载阶段componentWillUnmount

而 Hooks 的核心是 useEffect——一个基于依赖数组的副作用执行机制。useEffect 并不关心组件当前处于哪个生命周期阶段,它只关心“依赖项是否发生了变化”。因此,与其去机械地一对一映射,不如理解副作用与依赖之间的关系。

常见生命周期到 Hooks 的映射

1. componentDidMount:挂载后执行一次

类组件:

class Example extends React.Component {
  componentDidMount() {
    console.log('组件已挂载');
    // 发起数据请求
  }
  render() { return <div>Hello</div>; }
}

函数组件:

function Example() {
  useEffect(() => {
    console.log('组件已挂载');
    // 发起数据请求
  }, []); // 空依赖数组,只在首次渲染后执行一次
  return <div>Hello</div>;
}

注意:useEffect 在严格模式(Strict Mode)下,React 18+ 会故意执行两次挂载/卸载以暴露副作用逻辑缺陷,这在 componentDidMount 中不会发生。因此,如果你的副作用中存在副作用清理问题,严格模式会提前暴露出来。

2. componentDidUpdate:依赖变化时执行

类组件需要手动比较新旧 props/state:

componentDidUpdate(prevProps, prevState) {
  if (prevProps.userId !== this.props.userId) {
    console.log('userId 变化了');
    this.fetchData(this.props.userId);
  }
}

函数组件直接用依赖数组声明:

useEffect(() => {
  console.log('userId 变化了');
  fetchData(userId);
}, [userId]); // userId 变化时执行

这种方式更简洁,且无需手动比较,React 自动追踪依赖变化。

3. componentWillUnmount:组件卸载前清理

类组件:

componentDidMount() {
  this.timer = setInterval(() => {}, 1000);
}
componentWillUnmount() {
  clearInterval(this.timer);
}

函数组件:

useEffect(() => {
  const timer = setInterval(() => {}, 1000);
  return () => {
    clearInterval(timer); // 清理函数,在组件卸载时执行
  };
}, []);

useEffect 的返回函数就是清理函数。它不仅在卸载时执行,也会在下一次 effect 执行前运行,保证每次 effect 都有机会清理上一次的副作用。

4. shouldComponentUpdate:控制是否重新渲染

类组件中可以通过 shouldComponentUpdatePureComponent 来阻止不必要的渲染。在函数组件中,对应的工具是 React.memouseMemo / useCallback

  • React.memo:包裹组件,进行 props 浅比较,相当于 PureComponent
const MyComponent = React.memo(function MyComponent({ data }) {
  return <div>{data}</div>;
});
  • useMemo / useCallback:在组件内部缓存值和函数引用,避免子组件因 prop 引用变化而重新渲染。

注意:函数组件本身不具备 shouldComponentUpdate 的实质阻止渲染能力(那是外部 React.memo 的职责),但 Hooks 通过缓存机制减少了不必要的重计算和传递。

5. getDerivedStateFromProps:从 props 派生 state

类组件中这个静态方法比较特殊,根据 props 更新 state。在函数组件中,通常不需要这种模式,因为你可以直接在渲染期间计算派生值:

function Example({ user }) {
  // 直接计算,无需 state
  const isLoggedIn = !!user;
  return <div>{isLoggedIn ? '已登录' : '请登录'}</div>;
}

如果状态需要缓存以进行性能优化,可以使用 useMemo

const derived = useMemo(() => computeExpensive(user), [user]);

反模式警告:很多人会用 useEffect + setState 来模拟 getDerivedStateFromProps,这会导致不必要的额外渲染,应尽量避免。除非派生逻辑非常复杂且必须异步处理,否则优先在渲染期间直接计算。

6. componentDidCatch / getDerivedStateFromError:错误边界

错误边界只能在类组件中实现,目前函数组件没有等价的 Hooks。React 官方文档明确指出,错误边界必须用类组件编写。如果需要错误边界功能,你仍然需要写一个类组件。

完整对照表

| 类组件生命周期 | 函数组件 Hooks 映射 | 注意事项 |
|--------------|------------------|---------|
| componentDidMount | useEffect(fn, []) | 严格模式下执行两次 |
| componentDidUpdate | useEffect(fn, [deps]) | 不需要手动比较前后值 |
| componentWillUnmount | useEffect 返回的清理函数 | 每次 effect 重新执行前也会清理 |
| shouldComponentUpdate | React.memouseMemouseCallback | 作用层面不同,注意区分 |
| getDerivedStateFromProps | 渲染期间直接计算 或 useMemo | 避免使用 useEffect + setState |
| componentDidCatch | 无,错误边界必须用类组件 | React 官方声明 |
| getSnapshotBeforeUpdate | 罕见,无直接等价 | 可通过 useLayoutEffect 配合 ref 模拟,不推荐 |

常见陷阱:用 Hooks 生搬硬套生命周期

许多新手会试图把类组件的生命周期“翻译”成 Hooks,而不是真正理解声明式副作用。这种生搬硬套可能导致以下问题:

  • 过度使用 useEffect:把本应在渲染期间计算的值放到 useEffect 中更新 state,造成二次渲染。
  • 依赖缺失或误用useEffect 依赖数组不完整,导致闭包陷阱。
  • 清理函数遗漏:忘记返回清理函数,导致内存泄漏(定时器、订阅未清除)。

最佳实践是:忘掉生命周期,拥抱副作用依赖。思考“这个副作用依赖于哪些数据?”,然后用 useEffect 清晰地表达出来即可。

实战示例:一个订阅场景

类组件:

class FriendStatus extends React.Component {
  componentDidMount() {
    subscribeToFriendStatus(this.props.friendId, this.handleStatusChange);
  }
  componentDidUpdate(prevProps) {
    if (prevProps.friendId !== this.props.friendId) {
      unsubscribeFromFriendStatus(prevProps.friendId, this.handleStatusChange);
      subscribeToFriendStatus(this.props.friendId, this.handleStatusChange);
    }
  }
  componentWillUnmount() {
    unsubscribeFromFriendStatus(this.props.friendId, this.handleStatusChange);
  }
  handleStatusChange = (status) => {
    this.setState({ status });
  }
  render() { /* ... */ }
}

函数组件(干净利落):

function FriendStatus({ friendId }) {
  const [status, setStatus] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setStatus(status);
    }
    subscribeToFriendStatus(friendId, handleStatusChange);
    return () => {
      unsubscribeFromFriendStatus(friendId, handleStatusChange);
    };
  }, [friendId]); // 依赖 friendId,变化时自动重新订阅并清理旧订阅

  return /* 渲染 status */;
}

不再需要分散在三个生命周期中的逻辑,一个 useEffect 清晰地表达了“当 friendId 变化时,需要订阅新的状态并取消旧的订阅”的完整意图。这就是 Hooks 带来的优势:按关注点组织代码,而非按生命周期拆分