资讯动态

React生命周期全解析:挂载、更新、卸载与Hooks实战

发布时间:2026/10/16 2:08:10 来源:尧图企业网站定制
React 生命周期这个词不少同学面试时背得滚瓜烂熟一写业务就露馅数据请求到底放哪里定时器在哪个环节清为什么组件明明已经移出页面了接口回调里还在 setState这些问题表面上是写法不对根子上其实是生命周期模型没建立。我见过太多项目里堆满了无意义的重复渲染、卸载后还在执行的回调、以及因为清理工作缺失导致的内存泄漏。这篇文章我就从挂载到卸载把这套机制连原理带实战完整过一遍适合刚接触生命周期的新手也适合写了两三年 React 但一直凭感觉写业务的中级前端。看完你能回答的不只是钩子顺序而是这段代码到底该在哪个时机跑、跑完要不要清理。1. 生命周期不是一条直线先认清阶段顺便破两个误区1.1 三个阶段与两条技术路线React 组件从被创建到被销毁宏观上就三个阶段挂载mount、更新update、卸载unmount。挂载是组件第一次被插入页面更新是 props 或 state 变化触发重渲染卸载是组件从界面上移除。这个概念每个前端都听过但落到代码上就要分两条路线Class 组件用一套以方法名命名的生命周期钩子函数组件用 Hooks主要是 useEffect来表达在某个时机做某件事。严格说这两套东西不是一一对应的关系更不是把 componentDidMount 翻译成 useEffect 就完事。我建议大家把生命周期理解成一段三段式管线而不是一条从 A 走到 B的直线。React 内部先计划render 阶段也叫协调阶段再动手commit 阶段真实写入 DOM。计划阶段负责计算UI 应该长什么样这个阶段是纯函数式的可以被反复调用commit 阶段才真正执行 DOM 修改和副作用。引入并发特性之后计划阶段甚至可能因为高优先级任务插队而被打断、被丢弃、再重新执行。理解了这一点很多为什么 effect 执行了两次为什么 console.log 打了多次的疑惑都能迎刃而解。1.2 误区一把 render 当成页面重绘初学最典型的误区以为 render或者函数组件的函数体就等于页面重绘所以要把事件绑定、定时器启动、数据请求都写在里面。实际上 render 阶段只是根据当前 props 和 state 算出 UI 描述这个过程必须是纯函数式的不能有副作用。副作用订阅、请求、DOM 操作、写日志都应该推迟到 commit 之后的生命周期去执行。如果你在函数体里直接 fetchReact 会给你警告更可怕的是因为 render 阶段可能执行多次你还可能发出多个重复的网络请求白白浪费流量不说界面状态还会被搞乱。1.3 误区二以为卸载后什么都不用管另一个高频问题很多业务代码只在挂载时做了事卸载时不做任何清理。定时器还在跑、全局监听器还挂着、异步请求还没有取消等组件被移除回调里的 setState 就会命中对已卸载组件执行状态更新的警告。新版 React 弱化了这个警告但状态泄漏、监听器堆积、无效计算这些问题仍然客观存在。所以我向来建议把卸载阶段当成独立章节来学习而不是最后一步顺便清一下。这也是我写这篇文章的一个直接原因。2. 挂载阶段组件出生时每一步在做什么2.1 Class 组件出生顺序从 constructor 到 componentDidMountClass 组件的挂载顺序是固定的一共有四个步骤constructor初始化本地 state、绑定实例方法用箭头函数定义方法也可以省掉这一步static getDerivedStateFromProps在 render 之前根据 props 派生 state返回值会合并进 staterender产出虚拟 DOM 描述componentDidMount真实 DOM 已经挂载完毕可以操作 DOM、发起请求、添加监听顺序背后有个容易忽略的细节constructor里不能调用setState只能直接给this.state赋值。还有一个官方明确点名的反模式——把 props 复制到 state。比如很多人写this.state { name: props.name }看起来没什么问题但父组件更新了props.name子组件 state 里的值不会跟着变之后所有基于这个 state 的逻辑都会用旧值。正确做法是直接读props要么用getDerivedStateFromProps做同步派生要么干脆用key强制重建组件这个技巧我在第五部分会展开。componentDidMount是副作用的高发地请求数据、加全局事件监听、初始化第三方库。它只在浏览器端执行一次服务端渲染时不会执行所以很多同构项目会把是否在浏览器环境的判断逻辑放在这里。要记住一个原则挂载时做了什么非对称的事开了定时器、绑了监听、建了连接卸载时就必须对称地收回来。2.2 函数组件如何表达挂载useEffect 与 useLayoutEffect 的时机差异函数组件没有 constructor也没有 componentDidMount但可以用 useEffect 表达挂载时机的效果useEffect(() { // 这里等价于 componentDidMount 的时机 fetchData(); }, []);空依赖数组[]表示只在组件挂载后执行一次。但我必须强调一个认知差useEffect 默认在每次渲染提交后都会执行空数组只是让它只执行一次返回的清理函数会在卸载时执行也会在依赖变化导致 effect 重跑之前执行。所以 useEffect 本质上是挂载、更新、卸载三合一的生命周期表达不是 componentDidMount 的严格替身。还有一个容易被忽略的变体useLayoutEffect。它和 useEffect 的区别在执行时机——useLayoutEffect在 DOM 变更后、浏览器绘制前同步执行useEffect在浏览器绘制完成后异步执行。如果需要 DOM 更新后立刻读取布局属性比如测量元素高度、同步调整滚动位置用useLayoutEffect能避免先渲染旧位置再跳新位置的闪烁问题。我的个人建议是默认能用 useEffect 就用它只有明确需要同步读取布局时才换成 useLayoutEffect。2.3 挂载时该不该请求数据、该不该顺手改 state业务里问得最多的两个挂载问题这里一起说。第一数据请求。放在 componentDidMount /useEffect(..., [])是主流做法因为此时 DOM 已就绪而且只执行一次。但挂载请求天然存在竞态风险用户很快离开页面请求还没返回回调里的 setState 已经没有意义。规范做法是配合 AbortController 取消请求或者在卸载时打一个已被卸载的标记位回调里判断一下再决定是否更新状态。第四部分的完整代码会演示这个方案。第二挂载后改 state。严格来说挂载之后立刻 setState 会触发一次额外的重复渲染所以能不改就不改。需要根据 props 派生 state 的场景应该用 getDerivedStateFromPropsClass或渲染中调整 state函数组件而不是在 effect 里再 set 一遍。你可能会觉得在 effect 里 setState也没多大事但在大列表、高频更新场景里多余的那一次渲染就会拉低性能而且这种写法很容易越用越多最后整个组件都在渲染→设置→再渲染的循环里打转。3. 更新阶段重渲染的触发源、拦截手段和边界3.1 哪些操作会让组件重新执行 render更新阶段的触发源主要有四类父组件重新渲染、props 变化、state 变化、Context 变化。这里最容易被忽视的是第一条——父组件只要 setState即使传给子组件的 props 完全没变子组件默认也会跟着 render 一次。React 的默认行为就是父动子动这是为了简单和一致代价就是无谓渲染。所以 React 才提供了PureComponent、shouldComponentUpdate、React.memo这些拦截手段目的都是同一件事让没变化的组件跳过重复工作。Class 组件的更新顺序是static getDerivedStateFromPropsshouldComponentUpdate返回 false 可以跳过后续 render 与 commitrendergetSnapshotBeforeUpdate拿到更新前的 DOM 快照componentDidUpdateDOM 已更新可以对比前后 props/state 做后续操作这里有个反直觉的点getDerivedStateFromProps在挂载和更新时都会执行而getSnapshotBeforeUpdate只在更新时执行。很多文档把它们归类到挂载/更新两个章节实现上它们却承担着跨阶段的职责。搞混这一点的人在排查为什么挂载时打印不出来快照值时会浪费不少时间。3.2 shouldComponentUpdate、PureComponent 与 memo拦截重渲染的正确姿势shouldComponentUpdate是 Class 组件最早期的拦截手段一般配合浅比较来用。React.PureComponent内置了 props/state 的浅比较省去手写。函数组件对应React.memo同样做浅比较也可以传第二个参数自定义比较函数。我踩过的一个典型坑把可变对象直接放进 state每次更新都原地修改而不是生成新引用结果浅比较完全失效。比如state.list.push(item)之后 setStatelist 的引用没变PureComponent 浅比较认为 state 没变化直接跳过更新页面不刷新。解决思路很简单state 保持不可变更新用展开语法或不可变库生成新对象让 memo 和 PureComponent 能正确感知变化。需要清醒的是拦截渲染只能解决不必要的渲染不能解决渲染了但数据没变的业务错误。memo 的浅比较判断为相等就压根不会执行 render如果你在里面依赖了一个外部可变对象等于是埋了一颗数据更新但视图不刷新的雷。3.3 getSnapshotBeforeUpdate滚动位置这类的快照需求什么时候用到getSnapshotBeforeUpdate 在业务里用得非常少但真正需要它的时候没有替代品。经典场景是聊天列表新消息来了如果用户正在往上翻历史你不想强制把滚动条拉到底部如果用户本来就在底部就要平滑滚下去。实现方案就是在 getSnapshotBeforeUpdate 里记住更新前的 scrollHeight 和 scrollTop在 componentDidUpdate 里对比差值决定要不要调整滚动位置。函数组件没有直接对应的 API常见做法是用useLayoutEffect配合 ref 手工实现在浏览器绘制前读完旧值、写入新位置再把闪烁控制在用户感知不到的范围。对大多数业务来说这个生命周期的使用频率很低但面试时能讲清楚为什么不能直接在 render 里读 DOM基本就能证明你对计划阶段 vs 提交阶段的理解到位了。3.4 更新阶段的函数组件依赖数组、闭包与不该重跑的 effect函数组件更新阶段的重点全在依赖数组上。useEffect(fn, deps)中 deps 变化fn 就会重新执行依赖数组里如果漏了某个变量还会引发闭包捕获旧值的经典 bug。典型现象是定时器每次读到的 count 都是初始值 0因为 effect 只在挂载时执行了一次闭包里捕获的是那次执行时的 count。排查思路分两步。第一步把所有 effect 里用到、且可能变化的变量都放进依赖数组这个是最基础的自查。第二步如果遇到只想拿到最新值又不想重跑 effect的情况用useCallback配合useRef来稳定引用。还有一个更底层的技巧如果一个 effect 因为依赖的对象每次渲染都重建而反复执行优先检查那个对象的生成处是否应该用useMemo/useCallback稳定下来而不是在依赖数组里写空数组来骗过 ESLint。骗 eslint 的代码最后都会变成线上 bug 来找你还债。4. 卸载阶段清理工作与内存泄漏的真实案例4.1 componentWillUnmount 的职责边界componentWillUnmount 是 Class 组件卸载前唯一执行的钩子它的职责是撤销挂载时留下的所有副作用移除全局事件监听、清除定时器、断开 WebSocket 连接、取消订阅。注意卸载阶段组件 DOM 即将移除不能再做 DOM 操作也不能在该阶段发 setState。实际项目里最常见的泄漏源头就是 componentDidMount 里加了window.addEventListener却忘了在 componentWillUnmount 里 removeEventListener。页面来回切换几次之后事件回调叠了一堆每次点击都触发 N 份重复逻辑内存和计算一起上涨。这种问题在开发环境特别隐蔽因为刷新一下页面内存就重置了线上环境里用户不刷新持续浏览半小时卡顿感就明显了。我习惯把挂载时添加了什么卸载时就移除什么写成 checklist每次写完挂载逻辑立刻检查对称性。4.2 useEffect 返回函数定时器、监听器、网络请求的统一清理函数组件把对称清理这件事简化成了一个结构useEffect 返回的函数就是清理回调它既会在卸载时执行也会在依赖变化导致 effect 重跑之前执行。看一个定时器例子useEffect(() { const timer setInterval(() { setCount(c c 1); }, 1000); return () clearInterval(timer); }, []);这段代码的含义是挂载后启动定时器卸载时清除定时器。如果依赖数组里有值清理函数会在重跑 effect 之前先执行再做下一次的启动。所以 useEffect 一个结构就同时覆盖了 componentDidMount、componentDidUpdate、componentWillUnmount 三类职责这也是为什么函数式组件的写法更推荐从副作用管理的角度去思考而不是从我要在哪个钩子里写代码的角度去思考。网络请求的清理我用得最多的是 AbortControlleruseEffect(() { const controller new AbortController(); fetch(/api/list, { signal: controller.signal }) .then(res res.json()) .then(setList) .catch(err { if (err.name ! AbortError) { // 这里才是真实的错误处理 } }); return () controller.abort(); }, []);组件卸载时请求会被主动取消回调不再执行从根本上避免了已经卸载还在 setState的竞态问题。如果你维护的是老项目没法大规模改造请求层至少可以在 useEffect 里放一个let cancelled false的标记回调执行前判断一下也能处理大部分问题。4.3 StrictMode 的卸载预演为什么会吓到新人很多人在开发环境加了 StrictMode第一次跑就发现挂载后 effect 执行了两次中间还夹着一次清理→再次挂载的过程第一反应是代码写错了。其实这是 React 开发模式下的故意行为通过额外模拟一次卸载再重挂载来暴露那些没有正确清理的副作用代码。生产环境不会做这个动作。理解这一点之后StrictMode 其实是一把免费的清理检查尺。如果你的 effect 没写返回的清理函数StrictMode 会在开发阶段直接让重复执行的问题暴露出来如果你的清理函数写得不对比如清理时机不完整StrictMode 同样能帮你提前发现问题。所以我的建议是开发环境不要为了消灭这个双重执行而屏蔽 StrictMode它逼你写干净的副作用长期来看能省掉大量线上排查时间。5. Class 与 Hook 的对照迁移一张表看懂外加三个易错点5.1 生命周期方法到 Hooks 的映射表Class 方法函数组件对应写法关键说明constructoruseState/useReducer 初始值 普通常量不需要 constructor 这种构造函数语义getDerivedStateFromProps渲染中调整 state或用 key 强制重建不是 1:1 映射别在 effect 里 setState 替代componentDidMountuseEffect(fn, [])仅在挂载后执行componentDidUpdateuseEffect(fn)或带依赖数组依赖数组是核心控制手段componentWillUnmountuseEffect 返回的清理函数卸载时执行getSnapshotBeforeUpdateuseLayoutEffect ref需要手工读取 DOM 快照shouldComponentUpdateReact.memo浅比较没有原生 Hook 替代componentDidCatch无对应 Hook仍需错误边界类组件这张表可以解决 90% 的迁移疑问但要反复提醒一句useEffect 不是某个生命周期方法的转世它更像替换整个生命周期体系的触发机制。它默认在每次渲染提交后执行既覆盖了 didMount也覆盖了 didUpdate还覆盖了卸载清理。写代码时的思考方式应该从我要在哪个钩子里写变成这段副作用应该何时运行、何时清理、依赖哪些数据。5.2 迁移时最常见的三个坑第一个坑把 componentDidMount 里的代码原封不动搬进useEffect(..., [])结果代码里依赖了某个 props之后 props 变了 effect 却不重跑界面一直显示旧数据。正确做法是把代码拆成依赖具体数据的 effect而不是永远空数组。第二个坑在 useEffect 里 setState依赖数组又包含这个 state 本身结果形成设置→触发更新→再设置的循环。解决方法是看逻辑能不能改成基于上一个状态的计算用setState(prev ...)回调形式或者把依赖拆细、把计算逻辑移出 effect。第三个坑把 getDerivedStateFromProps 的props 变化同步到 state迁移成在 effect 里 setState导致多余的渲染和短暂旧值闪现。官方推荐的替代方案叫渲染期间直接调整状态写法是 if 条件判断加 setStateReact 会检测到并在当前渲染流程内立即用新 state 重新执行而不是推到下一个更新周期。5.3 没有 Hook 对应的场景错误边界与 key 重建有一类生命周期 Hook 无法直接覆盖错误边界。componentDidCatch和getDerivedStateFromError目前仍然只能写在 Class 组件里。如果你的项目是全函数组件又需要全局异常上报兜底正确的做法是保留一个类组件作为错误边界包装层把业务组件包在里面。这不是妥协而是设计上给的明确边界。getDerivedStateFromProps 的另一个常见用途——根据 props 变化重置 state——在 Hook 里如果嫌渲染中调整 state 的写法绕我强烈推荐直接用key强制重建组件。举个例子一个根据 userId 加载用户详情的区块给组件传key{userId}userId 变了组件重新挂载所有 state 自然重置。这是很多人不知道的实用技巧比在 effect 里重置省事得多也暗合生命周期设计的初衷——把重置转换成重新出生让代码状态从一开始就是干净的。最后分享一点我自己的习惯学生命周期不要靠背钩子顺序而是画一张我这段代码到底想在哪个时机执行什么的清单然后对应到 Class 和 Hook 各自的最优写法。挂载时做请求和订阅、更新时控制重渲染范围、卸载时对称清理所有副作用这三件事想清楚比记住那七八个方法名管用得多。如果你正在做旧项目从 Class 到 Hook 的迁移我建议从副作用审查入手先把所有 useEffect 的清理函数补全再谈依赖数组优化一步一步来坑会少很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑