在理解生命周期函数定义的基础上真正考验的是不同场景下如何选用以及避开那些容易踩的坑。下面从类组件和 Hooks 函数组件两条线用代码案例详细对比深入场景、注意事项和陷阱。一、类组件生命周期场景、坑点与案例1. 挂载阶段 Mountingconstructor(props)场景初始化内部 state、绑定事件处理函数。坑点不可调用setState不要做任何副作用如请求数据、订阅。如果忘记调用super(props)this.props会是undefined。正确示例constructor(props) { super(props); this.state { loading: true }; this.handleSave this.handleSave.bind(this); }componentDidMount场景发起网络请求、订阅全局事件、操作 DOM、开启定时器。坑点直接setState会触发二次渲染但只要用于初始化数据请求这是正常的。但要注意若在服务端渲染时调用会导致内存泄漏应避免。注册的监听器和定时器必须在componentWillUnmount中清除否则卸载后继续执行会导致状态更新错误。代码案例componentDidMount() { this.fetchUser(); window.addEventListener(resize, this.handleResize); this.timer setInterval(this.tick, 1000); } componentWillUnmount() { window.removeEventListener(resize, this.handleResize); clearInterval(this.timer); }不推荐用法在componentWillMount中请求数据废弃原因异步渲染下该钩子可能被多次调用请求会重复。所以请求请放在componentDidMount。2. 更新阶段 UpdatingshouldComponentUpdate(nextProps, nextState)场景性能优化避免不必要的渲染。默认返回true。坑点若错误地返回false会导致组件无法响应 props 或 state 的变化。深层比较代价可能比渲染还高一般推荐使用PureComponent或React.memo。案例shouldComponentUpdate(nextProps) { // 只有列表数据真正变化时才更新 return nextProps.items ! this.props.items; }getSnapshotBeforeUpdate(prevProps, prevState)场景需要在 DOM 更新前捕获信息如滚动位置更新后配合componentDidUpdate恢复。坑点忘记返回值会导致componentDidUpdate的第三个参数为undefined。此方法必须和componentDidUpdate配合使用。代码案例保持聊天框滚动位置getSnapshotBeforeUpdate(prevProps) { 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) { this.listRef.current.scrollTop this.listRef.current.scrollHeight - snapshot; } }componentDidUpdate(prevProps, prevState, snapshot)场景根据变化后的 props 重新请求数据、操作 DOM。致命陷阱无限循环如果直接在里面无条件调用setState或触发状态更新且未做对比判断就会死循环。必须先比较新旧 props 或 state。错误案例 vs 正确案例// ❌ 错误每次更新都会请求导致无限循环 componentDidUpdate() { this.fetchData(this.props.userId); } // ✅ 正确仅在 userId 变化时请求 componentDidUpdate(prevProps) { if (this.props.userId ! prevProps.userId) { this.fetchData(this.props.userId); } }3. 卸载阶段 UnmountingcomponentWillUnmount场景清理所有副作用。坑点在这里调用setState会触发警告且无效组件已卸载。此外忘记取消异步请求的setState会导致内存泄漏和 React 警告。异步请求安全案例componentDidMount() { this.cancelled false; fetch(/api/user/${this.props.id}) .then(res res.json()) .then(data { if (!this.cancelled) { this.setState({ user: data }); } }); } componentWillUnmount() { this.cancelled true; // 标记已卸载避免 setState }4. 错误处理 Error Boundary场景捕获子树渲染错误显示降级 UI。坑点无法捕获自身抛出的错误只能捕获子组件。也无法捕获事件处理器、异步代码中的错误需要配合try/catch。代码class ErrorBoundary extends React.Component { state { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch(error, errorInfo) { // 上报错误 logService.report(error, errorInfo); } render() { if (this.state.hasError) return h1Something went wrong./h1; return this.props.children; } }二、函数组件 Hooks 模拟生命周期场景与陷阱函数组件没有生命周期方法一切靠 Hooks 组合。最核心的是useEffect。1. 模拟 componentDidMount场景首次挂载后执行请求、订阅。代码useEffect(() { fetchUser(); const subscription eventSource.subscribe(); return () { subscription.unsubscribe(); // 模拟 componentWillUnmount }; }, []); // 空依赖数组 → 只在挂载时运行一次坑点依赖数组[]必须真实无依赖否则eslint-plugin-react-hooks会报警告。如果内部引用了 props 或 state应该列入依赖。函数中读取的 state 会形成闭包获取到的是初次渲染的值每次更新都会变化实际上如果依赖为空内部引用的变量永远是初始值。若需要最新值可用useRef。2. 模拟 componentDidUpdate场景当特定 props 或 state 变化时执行副作用如重新获取数据。代码useEffect(() { if (userId) { fetchUserData(userId); } }, [userId]); // userId 变化时执行陷阱1忽略依赖导致闭包陈旧值// ❌ 错误count 陈旧每次打印的 count 都是初始的 0 useEffect(() { const timer setInterval(() { console.log(count); // 永远打印 0 }, 1000); return () clearInterval(timer); }, []);解决将count列入依赖或使用函数式更新setCount(c c 1)或用useRef存储最新 count。陷阱2异步请求在组件卸载后更新状态即使依赖变化触发新请求前一次请求可能还未完成。可使用标志位或AbortController。useEffect(() { let cancelled false; fetch(/api/user/${userId}) .then(res res.json()) .then(data { if (!cancelled) setUser(data); }); return () { cancelled true; }; }, [userId]);3. 模拟 componentWillUnmount代码在useEffect返回清理函数。useEffect(() { const onResize () { /* ... */ }; window.addEventListener(resize, onResize); return () window.removeEventListener(resize, onResize); }, []);4. useLayoutEffect — 同步读取布局场景需要同步获取 DOM 尺寸并在修改后立即渲染避免闪烁。如滚动到指定位置、测量元素。对比 useEffectuseEffect是在屏幕绘制后执行如果修改 DOM 可能导致闪屏。useLayoutEffect(() { // 获取元素尺寸并马上调整 const rect ref.current.getBoundingClientRect(); setHeight(rect.height); }, []);坑点会阻塞渲染需谨慎使用。大多数情况useEffect足够。5. useRef 突破闭包陷阱场景需要在异步回调中获取最新 state 或 props但又不想添加依赖。const latestCount useRef(count); useEffect(() { latestCount.current count; // 每次渲染更新 ref }); useEffect(() { const timer setInterval(() { console.log(latestCount.current); // 总是最新值 }, 1000); return () clearInterval(timer); }, []);三、常见坑点深度对比案例陷阱对比getDerivedStateFromProps滥用 vs 正确替代错误用法用 props 复制到 state导致后续 props 更新无法同步。// ❌ 仅在挂载时复制后续 props 变化不会更新 state state { email: this.props.email };如果确实需要根据 props 重置 state应使用getDerivedStateFromProps但更好的方式是用完全受控组件或使用 key 重置。// ✅ 使用 key 强制重新挂载 EmailInput key{userId} email{user.email} /getDerivedStateFromProps会让代码冗长通常只在罕见场景如 state 必须严格映射 props 变化同时还要维护内部修改使用。陷阱对比shouldComponentUpdate vs PureComponent vs React.memo类组件React.PureComponent自动浅比较 props 和 state。函数组件React.memo包裹。搭配useMemo和useCallback避免引用变化导致 memo 失效。const MyComponent React.memo(({ items, onItemClick }) { // 渲染 }); // 父组件中 const clickHandler useCallback((id) { /* ... */ }, []); const sortedItems useMemo(() items.sort(), [items]);陷阱在事件处理中直接使用 state 导致闭包陈旧值函数组件每次渲染都会创建新的函数若事件处理函数如定时器捕获了旧的 state解决方案是使用函数式更新。// ❌ 如果依赖变化频繁可能仍捕获旧值 useEffect(() { const id setInterval(() { setCount(count 1); // count 陈旧 }, 1000); return () clearInterval(id); }, [count]); // 每次 count 变化重建定时器不好 // ✅ 函数式更新 useEffect(() { const id setInterval(() { setCount(c c 1); }, 1000); return () clearInterval(id); }, []);四、场景对比类组件 vs 函数组件实现订阅类组件实现实时消息订阅class ChatRoom extends React.Component { componentDidMount() { this.sub MessageAPI.subscribe(this.props.roomId, this.handleNewMessage); } componentDidUpdate(prevProps) { if (prevProps.roomId ! this.props.roomId) { this.sub.unsubscribe(); this.sub MessageAPI.subscribe(this.props.roomId, this.handleNewMessage); } } componentWillUnmount() { this.sub.unsubscribe(); } handleNewMessage (msg) { this.setState(prev ({ messages: [...prev.messages, msg] })); } // ... }函数组件 Hooks 实现function ChatRoom({ roomId }) { const [messages, setMessages] useState([]); useEffect(() { const handleNewMessage (msg) { setMessages(prev [...prev, msg]); }; const sub MessageAPI.subscribe(roomId, handleNewMessage); return () sub.unsubscribe(); }, [roomId]); // roomId 变化时重新订阅清理旧订阅 // ... }Hooks 版本自动将挂载、更新、卸载的清理逻辑整合在一起避免了重复代码和漏清理的风险。五、最佳实践总结数据请求统一在componentDidMount或useEffect([], [])中发起注意清理。副作用清理务必返回清除函数避免内存泄漏。避免不必要的重新渲染类组件用PureComponent或shouldComponentUpdate函数组件用React.memouseCallback/useMemo。小心闭包陈旧值使用useRef或函数式更新setState(prev ...)。谨慎使用getDerivedStateFromProps优先考虑受控组件或key重置。错误边界用类组件实现包裹可能出错的子树同时注意无法捕获异步错误。useEffect与useLayoutEffect绝大多数场景用前者只有当 DOM 操作需要同步读取/修改避免视觉抖动时才用后者。严格遵循 eslint-plugin-react-hooks 规则它可以帮助你捕获缺失的依赖。理解生命周期的本质就是管理组件的资源申请与释放。用对场景、避开陷阱才能写出健壮的 React 应用。