一、总体时间线(一张图记一辈子)
二、四大阶段 8 个场景逐条拆解
阶段 1:实例创建(Initialization)
钩子 | 触发时机 | 能做的事 | 不能做的事 | 源码位置(vue2) | 常见坑 |
---|---|---|---|---|---|
beforeCreate | 实例刚 new 完,inject、props、methods、data、computed、watch 全部未初始化 | 可以:打印 this,看到裸实例;注册全局事件总线 | 不能:访问 this.xxx、this.$data、DOM | core/instance/init.js -> initLifecycle | 试图读取 data 得到 undefined |
setup() (仅 Vue3) | 组件实例尚未创建,但 props 已解析 | 可以:使用 ref/reactive 创建响应式数据、调用组合式函数 | 不能:使用 this (不存在);访问 DOM | runtime-core/component.ts | 忘记 return 模板需要的绑定 |
created | 完成了数据观测、computed、methods、watch 的初始化,但 $el 尚不可用 | 可以:发 ajax,访问 data/props/computed;写 this.xxx | 不能:访问 DOM($el 为 undefined) | core/instance/init.js -> callHook | 在 SSR 中此阶段 DOM 仍未创建 |
调试技巧
// main.js (Vue2)
console.log('new Vue 前', Date.now());
new Vue({
beforeCreate() { debugger; }, // Chrome 停在这里,看 call stack
created() { console.log('$el:', this.$el); /* undefined */ }
})
阶段 2:模板编译 + DOM 挂载(Mounting)
钩子 | 触发时机 | 能做的事 | 不能做的事 | 源码位置 | 常见坑 |
---|---|---|---|---|---|
beforeMount | 模板/渲染函数已编译,但虚拟 DOM 第一次 patch 前 | 可以:最后一次修改不触发额外渲染的数据 | 不能:访问真实 DOM;读取 $refs | core/instance/lifecycle.js -> mountComponent | 用 $refs 会得到 undefined |
mounted | 真实 DOM 已插入页面,$el 已指向元素 | 可以:操作 DOM、第三方库初始化、拿 $refs | 不能:保证所有子组件也 mounted(异步子组件可能还没好) | 同上 | 父 mounted 时子组件不一定 mounted |
可视化验证
<template>
<div ref="box">hello</div>
</template>
<script>
export default {
beforeMount() { console.log('beforeMount', this.$refs.box); /* undefined */ },
mounted() { console.log('mounted', this.$refs.box); /* <div> */ }
}
</script>
阶段 3:响应式更新(Updating)
钩子 | 触发时机 | 能做的事 | 不能做的事 | 源码位置 | 常见坑 |
---|---|---|---|---|---|
beforeUpdate | 数据已变,虚拟 DOM 打补丁前 | 可以:访问现有 DOM 状态(旧值) | 建议:不要改数据,会触发二次更新 | core/observer/scheduler.js -> flushSchedulerQueue | 死循环:此处改同一响应式属性 |
updated | DOM 已打补丁完成 | 可以:操作更新后的 DOM | 不能:改数据后直接读 DOM(DOM 可能还没 flush 完) | 同上 | 在 updated 里再改数据 → 无限循环 |
死循环 DEMO
export default {
data: () => ({ n: 0 }),
updated() {
this.n++; // ❌ 无限递归
}
}
解决:用 nextTick
或 watch
替代。
阶段 4:卸载(Unmount / Teardown)
钩子 | 触发时机 | 能做的事 | 不能做的事 | 备注 |
---|---|---|---|---|
beforeUnmount(Vue3) / beforeDestroy(Vue2) | 组件即将被卸载 | 可以:清理定时器、取消订阅、解绑全局事件 | 不能:访问 DOM 已卸载的子组件 | Vue3 改名更语义化 |
unmounted(Vue3) / destroyed(Vue2) | 组件完全卸载,所有指令、事件监听器已解绑 | 可以:记录日志 | 不能:访问实例上的响应式数据(已解除绑定) | keep-alive 缓存的组件不会触发 |
组合式清理示例
import { onBeforeUnmount, onUnmounted } from 'vue';
export default {
setup() {
const timer = setInterval(() => {}, 1000);
onBeforeUnmount(() => clearInterval(timer));
onUnmounted(() => console.log('组件已销毁'));
}
}
三、keep-alive 专属钩子
Vue2 | Vue3 | 触发时机 |
---|---|---|
activated | onActivated | 被 keep-alive 缓存的组件再次显示 |
deactivated | onDeactivated | 被 keep-alive 缓存的组件隐藏 |
使用场景:在列表页缓存滚动位置、停止/恢复定时器。
四、调试与可视化
-
Chrome DevTools → Sources → 在生命周期钩子中写
debugger;
-
安装 Vue Devtools → Timeline 面板直接看生命周期火焰图
-
Vue3 的
setup()
中可加console.trace()
查看调用栈
五、面试高频追问
问题 | 一句话回答 |
---|---|
为什么 beforeCreate 拿不到 data? | 此时 initState 尚未执行,data 未代理到 vm。 |
created 里能操作 DOM 吗? | 不能,$el 还是 undefined。 |
父 mounted 时子组件 mounted 了吗? | 不一定,子组件可以是异步组件。 |
updated 里再改同一份数据会怎样? | 再次触发 flush,导致无限循环。 |
Vue3 的 setup 对应 Vue2 的哪两个钩子? | 同时替代了 beforeCreate 和 created。 |
六、完整可运行 DEMO(Vue3 组合式)
<script type="module">
import { createApp, ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
createApp({
template: `
<button @click="count++">count: {{ count }}</button>
`,
setup() {
const count = ref(0);
console.log('setup');
onBeforeMount(() => console.log('onBeforeMount'));
onMounted(() => console.log('onMounted'));
onBeforeUpdate(() => console.log('onBeforeUpdate'));
onUpdated(() => console.log('onUpdated'));
onBeforeUnmount(() => console.log('onBeforeUnmount'));
onUnmounted(() => console.log('onUnmounted'));
return { count };
}
}).mount('#app');
</script>
<div id="app"></div>
打开浏览器控制台,点击按钮即可看到完整生命周期日志。