从类组件迁移到函数组件时,最大的困惑之一就是:原来熟悉的生命周期方法(如 componentDidMount、componentDidUpdate、componentWillUnmount)在 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:控制是否重新渲染
类组件中可以通过 shouldComponentUpdate 或 PureComponent 来阻止不必要的渲染。在函数组件中,对应的工具是 React.memo 和 useMemo / 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.memo、useMemo、useCallback | 作用层面不同,注意区分 |
| 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 带来的优势:按关注点组织代码,而非按生命周期拆分。