当前位置: 首页 > news >正文

react生命周期

在理解生命周期函数定义的基础上,真正考验的是不同场景下如何选用,以及避开那些容易踩的坑。下面从类组件和 Hooks 函数组件两条线,用代码案例详细对比,深入场景、注意事项和陷阱。


一、类组件生命周期:场景、坑点与案例

1. 挂载阶段 Mounting

constructor(props)
  • 场景:初始化内部 state、绑定事件处理函数。
  • 坑点:不可调用setState,不要做任何副作用(如请求数据、订阅)。如果忘记调用super(props)this.props会是undefined
  • 正确示例
constructor(props) { super(props); this.state = { loading: true }; this.handleSave = this.handleSave.bind(this); }
componentDidMount
  • 场景:发起网络请求、订阅全局事件、操作 DOM、开启定时器。
  • 坑点
    1. 直接setState会触发二次渲染,但只要用于初始化数据请求,这是正常的。但要注意,若在服务端渲染时调用会导致内存泄漏,应避免。
    2. 注册的监听器和定时器必须在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. 更新阶段 Updating

shouldComponentUpdate(nextProps, nextState)
  • 场景:性能优化,避免不必要的渲染。默认返回true
  • 坑点:若错误地返回false,会导致组件无法响应 props 或 state 的变化。深层比较代价可能比渲染还高,一般推荐使用PureComponentReact.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. 卸载阶段 Unmounting

componentWillUnmount
  • 场景:清理所有副作用。
  • 坑点:在这里调用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 <h1>Something 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包裹。
  • 搭配useMemouseCallback避免引用变化导致 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 版本自动将挂载、更新、卸载的清理逻辑整合在一起,避免了重复代码和漏清理的风险。


五、最佳实践总结

  1. 数据请求:统一在componentDidMountuseEffect([], [])中发起,注意清理。
  2. 副作用清理:务必返回清除函数,避免内存泄漏。
  3. 避免不必要的重新渲染:类组件用PureComponentshouldComponentUpdate;函数组件用React.memo+useCallback/useMemo
  4. 小心闭包陈旧值:使用useRef或函数式更新setState(prev => ...)
  5. 谨慎使用getDerivedStateFromProps,优先考虑受控组件或key重置。
  6. 错误边界用类组件实现,包裹可能出错的子树,同时注意无法捕获异步错误。
  7. useEffectuseLayoutEffect:绝大多数场景用前者;只有当 DOM 操作需要同步读取/修改,避免视觉抖动时才用后者。
  8. 严格遵循 eslint-plugin-react-hooks 规则,它可以帮助你捕获缺失的依赖。

理解生命周期的本质,就是管理组件的资源申请与释放。用对场景、避开陷阱,才能写出健壮的 React 应用。

http://www.rkmt.cn/news/1506800.html

相关文章:

  • 别再让OCV把你吓懵了!用PT的set_timing_derate让时序分析更靠谱
  • 2026南京企业业主高频选择的 5 家危房检测房屋结构安全鉴定机构实地测评整理 - 科信检测
  • 跨省寄大件怎么最省钱?实测比价攻略来了 - 快递物流资讯
  • 横岗配镜真心不踩雷!这家38年老店是我配镜的终极归宿 - 资讯纵览
  • 深圳横岗配镜天花板|38年视光老品牌,终于找到全维度满分配镜标杆 - 资讯纵览
  • 写论文AI写作哪个软件比较好用?4款工具功能全面对比 - 掌桥科研-AI论文写作
  • 【JAVA毕设源码分享】springboot基于区块链的电子病历数据共享平台设计与实现(程序+文档+代码讲解+一条龙定制)
  • 2026广州遗产继承律所TOP4深度测评|湾区家事继承甄选指南:遗嘱拟定、房产分割、遗产公证、代位继承、股权继承、遗赠纠纷、家事应诉 - 资讯纵览
  • 黄埔科创创业指南:2026黄埔区OPC一人公司注册实操与代办避雷 - 资讯纵览
  • 2026年佛山除甲醛服务商横向测评:家具产业集聚区如何科学应对装修污染 - 环保除醛知识库
  • 美团Q1环比减亏60%,“零售+科技”新战略下盈利悬念待解
  • WeReader浏览器扩展终极指南:3步快速导出微信读书笔记
  • 2026 消防设施操作员实操题库 APP 精选 综合排名 + 场景速选 - 讲清楚了
  • 南京汽车音响改装哪家好?南京音乐人生总店:二十载技术筑基,本土老牌名店终结全城音改踩坑之路 - 音乐人生汽车音响
  • 2026绵阳企业业主高频选择的 5 家危房检测房屋结构安全鉴定机构实地测评整理 - 科信检测
  • FC-PBGA封装热管理设计:从P5021处理器实战解析散热原理与工程避坑
  • openclaw数字员工解决方案哪个生产商专业
  • 多核DSP架构解析:从MSC8113看嵌入式实时信号处理系统设计
  • 2026海南珠宝行业注册怎么选代办?权威五星财税机构实测榜单+资质办理避坑指南 - 资讯纵览
  • 3分钟将单张图片变专业PSD分层:Layerdivider终极指南
  • 2026年国产操作系统深度横评:从“可用”到“好用”的王者之争,谁才是关键基础设施的最优解? - 资讯纵览
  • 用Python打造你的专属XKCD风格密码生成器(附完整词库和Flask Web版)
  • HTML转Figma完整指南:5分钟实现网页到设计稿的智能转换
  • 无人机数据日志分析实战:用Python脚本把Pixhawk的.tlog文件转成可读CSV
  • 自主 AI 代理网络钓鱼风险与全维度防御体系研究
  • 从一道CTF题Fakebook,聊聊SQL注入绕过空格过滤的几种骚操作(附脚本)
  • 2026宁波市奉化区家里卫生间漏水、阳台漏水、楼顶漏水、阳台漏水、地下室渗水、阳光房漏水各种房屋漏水情况不用愁!售后无忧,线上质保可查。本地防水补漏公司为您排忧解难! - 防水百科
  • 2026广州合同审查律所TOP4深度测评|湾区商事风控甄选指南:合同审核、风险规避、条款修订、违约追责、纠纷预判、商事应诉 - 资讯纵览
  • 2026锦州本地土壤检测农田土壤检测哪家强?TOP 正规机构榜单 + 联系方式 - 鉴安检测
  • MPC8541E硬件规格书深度解析:选型、电源、时序与PCB设计实战指南