模块一 · 起步与基础
生命周期与调试:钩子该放在哪,问题该从哪查
生命周期的本质是"组件在某一刻能做什么"。这一节给出六个阶段的钩子对照、父子执行顺序与各自的使用边界,并把 Vue DevTools 的调试路径走一遍。
读完这一节,你能做到
- 说出组件从创建到卸载的六个阶段,以及每个阶段能做什么、不能做什么。
- 排出父子组件的钩子执行顺序,解释为什么是这个顺序。
- 判断一段逻辑该放
setup、onMounted、onUpdated还是watch。 - 用 Vue DevTools 与
onTrack/onTrigger定位"为什么更新了"。
一、六个阶段与对应钩子
Vue 3 组合式 API 里,生命周期是"注册函数",必须在 setup 同步执行期间调用。
| 阶段 | 时机 | 组合式 API | 能做什么 | 不能做什么 |
|---|---|---|---|---|
| 创建 | 组件实例初始化,props 已解析 | setup 本身 | 初始化状态、调用组合式函数 | 拿不到 DOM,拿不到 $el |
| 挂载前 | 模板已编译,尚未插入 DOM | onBeforeMount | 最后调整初始数据 | 读 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
规律可以概括为:创建与挂载是"父先子后、子完成后父收尾",卸载是"父先声明、子先完成"。
由此得到两条实践:
- 父组件
onMounted里可以安全访问子组件暴露的 DOM 与ref。 - 子组件
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 + nextTick | DOM 必须已存在且布局完成 |
| 依赖 DOM 的第三方图表 | onMounted 初始化,onBeforeUnmount 销毁 | 实例必须成对管理 |
| 派生数据 | computed | 纯计算,有缓存 |
| 用户点击后的请求 | 事件回调 | 不该由渲染触发 |
最常见的错误是"把所有初始化都塞进 onMounted"。实际上能在 setup 里同步完成的就不要延后,延后只会让时序更难推理。
七、用 Vue DevTools 定位问题
DevTools 里最常用的四块:
| 面板 | 解决什么问题 | 关键操作 |
|---|---|---|
| 组件树 | 这个组件是谁渲染的、props 是什么 | 选中组件看 props / setup state |
| 时间线 | 一次交互触发了哪些更新 | 录制后看事件与组件更新顺序 |
| Pinia | 状态从哪来、被谁改 | 看 mutation 时间线 |
| 性能 | 哪些组件渲染耗时长 | 看组件渲染耗时排序 |
三条排查路径:
- 页面不更新:先看组件树里的 state 是否已变 → 再看是否被
shallowRef/markRaw包住 → 最后看依赖是否被收集。 - 更新太频繁:用时间线录制 3 秒,找重复出现的组件;再用
onTrack/onTrigger确认是哪个属性触发的。 - 组件不卸载:看组件树里是否还在,配合
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 快得多。
八、常见坑
- 在
setup里读 DOM,得到null。 - 在
onMounted里改数据后立刻读 DOM,读到旧值。 - 在
onUpdated里改状态,造成无限更新。 - 定时器只在
onUnmounted清理,但组件被KeepAlive缓存,切走后仍在跑。 onErrorCaptured返回false后,全局错误上报收不到,线上问题无从查起。- 在
onBeforeUnmount里访问子组件,子组件可能已先销毁。
九、动手任务:自写 → AI 写 → 对比 → 取舍
四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。
① 自己写
- 写一个组件,在六个钩子里各打一条带组件名的日志,验证父子顺序。
- 用
KeepAlive包住两个页面,观察activated/deactivated与mounted/unmounted的差异。 - 故意在子组件
setup里抛错,验证onErrorCaptured与app.config.errorHandler的触发关系。 - 用 DevTools 时间线录制一次输入,找出被重复渲染的组件并说明原因。
② 让 AI 写同一题
把第 2 题(onMounted 里发请求)交给 AI,补一句 组件卸载后不能再更新状态。
③ 对比要盯的点
- 它在
onUnmounted里清理了定时器、事件监听或订阅吗? - 请求竞态它处理了吗?连续切三个 id,旧结果会盖掉新的。
- 它有没有用
onUpdated做数据初始化?那极易形成更新死循环。
④ 一句话取舍
它省掉的清理,在“快速切换三个 tab”时会在控制台留下什么?
十、自测
- 首次渲染时父子钩子的执行顺序是什么?
onMounted触发时 DOM 一定可用吗?什么情况下不是?nextTick解决什么问题?KeepAlive下为什么数据不刷新?onErrorCaptured捕获不到哪两类错误?
参考答案要点:父 setup→父 beforeMount→子→子 mounted→父 mounted;依赖父级布局时可能未完成;等批量更新刷新;组件未重新挂载,需在 activated 刷新;事件回调与异步 Promise。
配套题目
★ 越多越该先背;点题目可看完整答案与深入版。
提示:读完可以顺手做几道对应题目,检验是否真的理解