知

模块一 · 起步与基础

第 6 / 17 节18 分钟阅读型

生命周期与调试:钩子该放在哪,问题该从哪查

生命周期的本质是"组件在某一刻能做什么"。这一节给出六个阶段的钩子对照、父子执行顺序与各自的使用边界,并把 Vue DevTools 的调试路径走一遍。

读完这一节,你能做到

  1. 说出组件从创建到卸载的六个阶段,以及每个阶段能做什么、不能做什么。
  2. 排出父子组件的钩子执行顺序,解释为什么是这个顺序。
  3. 判断一段逻辑该放 setup、onMounted、onUpdated 还是 watch。
  4. 用 Vue DevTools 与 onTrack/onTrigger 定位"为什么更新了"。

一、六个阶段与对应钩子

Vue 3 组合式 API 里,生命周期是"注册函数",必须在 setup 同步执行期间调用。

阶段时机组合式 API能做什么不能做什么
创建组件实例初始化,props 已解析setup 本身初始化状态、调用组合式函数拿不到 DOM,拿不到 $el
挂载前模板已编译,尚未插入 DOMonBeforeMount最后调整初始数据读 DOM 尺寸
挂载后组件 DOM 已插入onMounted读 DOM、初始化第三方实例、发首屏请求假设父组件已完成挂载
更新前数据变化,重新渲染前onBeforeUpdate记录更新前的 DOM 快照在这里改状态(会再触发更新)
更新后DOM 已按新数据更新onUpdated依赖新 DOM 的计算、第三方实例同步在这里无条件改状态(死循环)
卸载前即将移除onBeforeUnmount清理定时器、解绑事件、取消请求依赖子组件仍然存在
卸载后已移除onUnmounted释放外部资源、断开连接访问 DOM

另外三个特殊钩子:

  • onActivated / onDeactivated:仅在 <KeepAlive> 缓存的组件上触发。
  • onErrorCaptured:捕获后代组件的错误,返回 false 阻止继续向上冒泡。
  • onServerPrefetch:SSR 期间预取数据,纯客户端项目用不到。

二、父子执行顺序

首次渲染:

父 setup → 父 onBeforeMount
→ 子 setup → 子 onBeforeMount → 子 onMounted
→ 父 onMounted

更新时:

父 onBeforeUpdate → 子 onBeforeUpdate → 子 onUpdated → 父 onUpdated

卸载时:

父 onBeforeUnmount → 子 onBeforeUnmount → 子 onUnmounted → 父 onUnmounted

规律可以概括为:创建与挂载是"父先子后、子完成后父收尾",卸载是"父先声明、子先完成"。

由此得到两条实践:

  1. 父组件 onMounted 里可以安全访问子组件暴露的 DOM 与 ref。
  2. 子组件 onMounted 里不要假设父组件已完成初始化;需要父级数据时,用 props 或 provide,不要靠"父组件应该已经准备好了"。

三、onMounted 与 nextTick 的关系

onMounted 触发时,组件的 DOM 已经插入文档,但有两类情况仍可能读不到预期值:

  • 依赖父级布局的尺寸:父级可能还没完成布局,读 offsetHeight 可能为 0。
  • 在 onMounted 里改数据后立刻读 DOM:Vue 的更新是异步批量的,改完数据 DOM 还是旧的,要 await nextTick()。
onMounted(async () => {
  box.value!.scrollTop = 999; // 读的是挂载后的真实 DOM
  await nextTick(); // 等这一轮更新刷新完
  console.log(list.value!.scrollHeight);
});

判断标准:要不要等"下一次渲染结果"。要,就 nextTick;只是读当前 DOM,直接读。

四、KeepAlive 下的生命周期

被 <KeepAlive> 缓存的组件,切走时不会 onUnmounted,而是 onDeactivated;切回时不会重新 onMounted,而是 onActivated。

这带来三个实际问题:

问题原因修法
回到列表页数据不刷新组件没重新挂载在 onActivated 里刷新数据
定时器一直在跑切走只是 deactivated在 onDeactivated 里暂停,onActivated 里恢复
滚动位置丢失或错乱缓存了 DOM 但未恢复滚动手动记录并恢复 scrollTop

同时要区分:onDeactivated 不等于卸载,不能在里面做"释放资源"的终态清理;真正的清理仍要写在 onUnmounted。

五、错误捕获:onErrorCaptured

onErrorCaptured((err, instance, info) => {
  reportToServer(err, info); // info 形如 "render function" / "setup function"
  return false; // 阻止继续向上冒泡
});

边界要点:

  • 只捕获后代组件的错误,不捕获自身 setup 抛出的错误。
  • 不能捕获事件回调、异步 Promise 里的错误,这些要自己 try/catch 或交给全局 window.onerror / unhandledrejection。
  • 返回 false 会阻止全局 app.config.errorHandler 收到该错误,排查时要注意。

生产项目至少要有两层:组件级 onErrorCaptured 做局部降级,app.config.errorHandler 做统一上报。

六、把逻辑放对位置

逻辑放置位置理由
初始化响应式状态setup 顶层只需要执行一次
首屏请求onMounted 或路由守卫需要组件上下文或导航完成
依赖 props 的请求watch(() => props.id, load, { immediate: true })props 变化要重新取数
读 DOM 尺寸onMounted + nextTickDOM 必须已存在且布局完成
依赖 DOM 的第三方图表onMounted 初始化,onBeforeUnmount 销毁实例必须成对管理
派生数据computed纯计算,有缓存
用户点击后的请求事件回调不该由渲染触发

最常见的错误是"把所有初始化都塞进 onMounted"。实际上能在 setup 里同步完成的就不要延后,延后只会让时序更难推理。

七、用 Vue DevTools 定位问题

DevTools 里最常用的四块:

面板解决什么问题关键操作
组件树这个组件是谁渲染的、props 是什么选中组件看 props / setup state
时间线一次交互触发了哪些更新录制后看事件与组件更新顺序
Pinia状态从哪来、被谁改看 mutation 时间线
性能哪些组件渲染耗时长看组件渲染耗时排序

三条排查路径:

  1. 页面不更新:先看组件树里的 state 是否已变 → 再看是否被 shallowRef/markRaw 包住 → 最后看依赖是否被收集。
  2. 更新太频繁:用时间线录制 3 秒,找重复出现的组件;再用 onTrack/onTrigger 确认是哪个属性触发的。
  3. 组件不卸载:看组件树里是否还在,配合 onUnmounted 日志确认清理函数是否执行。

onTrack/onTrigger 是调试依赖的利器:

import { onTrack, onTrigger, watchEffect } from "vue";

watchEffect(() => {
  onTrack((e) => console.log("track", e.key, e.target));
  onTrigger((e) => console.log("trigger", e.key, e.newValue));
  console.log(state.count);
});

它只在开发环境有效,能直接告诉你"谁读了这个属性、谁改了它",比到处 console.log 快得多。

八、常见坑

  1. 在 setup 里读 DOM,得到 null。
  2. 在 onMounted 里改数据后立刻读 DOM,读到旧值。
  3. 在 onUpdated 里改状态,造成无限更新。
  4. 定时器只在 onUnmounted 清理,但组件被 KeepAlive 缓存,切走后仍在跑。
  5. onErrorCaptured 返回 false 后,全局错误上报收不到,线上问题无从查起。
  6. 在 onBeforeUnmount 里访问子组件,子组件可能已先销毁。

九、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 写一个组件,在六个钩子里各打一条带组件名的日志,验证父子顺序。
  2. 用 KeepAlive 包住两个页面,观察 activated/deactivated 与 mounted/unmounted 的差异。
  3. 故意在子组件 setup 里抛错,验证 onErrorCaptured 与 app.config.errorHandler 的触发关系。
  4. 用 DevTools 时间线录制一次输入,找出被重复渲染的组件并说明原因。

② 让 AI 写同一题

把第 2 题(onMounted 里发请求)交给 AI,补一句 组件卸载后不能再更新状态。

③ 对比要盯的点

  • 它在 onUnmounted 里清理了定时器、事件监听或订阅吗?
  • 请求竞态它处理了吗?连续切三个 id,旧结果会盖掉新的。
  • 它有没有用 onUpdated 做数据初始化?那极易形成更新死循环。

④ 一句话取舍

它省掉的清理,在“快速切换三个 tab”时会在控制台留下什么?

十、自测

  1. 首次渲染时父子钩子的执行顺序是什么?
  2. onMounted 触发时 DOM 一定可用吗?什么情况下不是?
  3. nextTick 解决什么问题?
  4. KeepAlive 下为什么数据不刷新?
  5. onErrorCaptured 捕获不到哪两类错误?

参考答案要点:父 setup→父 beforeMount→子→子 mounted→父 mounted;依赖父级布局时可能未完成;等批量更新刷新;组件未重新挂载,需在 activated 刷新;事件回调与异步 Promise。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解