一、什么是生命周期
1.1 定义
Vue 生命周期 是指一个组件从创建 →挂载 →更新 →卸载 的整个过程。在这个过程中,Vue 会在每个关键节点自动调用一些特殊函数 ,这些函数就叫生命周期钩子(Lifecycle Hooks)。
关键点:钩子函数是 Vue 主动叫你的,不是你叫它。你只需要把代码写进对应的钩子里,Vue 会在合适的时机执行。
1.2 生活类比
把组件想象成一个"员工"的一生:
- 出生 → 接入职手续(created)
- 上岗 → 拿到工牌、坐到工位(mounted)
- 工作变动 → 调岗、改工资(updated)
- 离职 → 交还工牌、清理工位(unmounted)
每个节点公司都会通知你办手续,你不用自己盯,Vue 就是这个"公司"。
1.3 为什么要懂生命周期
因为不同阶段能干的事不一样:
- 想拿数据 → 得在
created之后(数据才成型) - 想操作 DOM → 得在
mounted之后(节点才上树) - 想清理定时器 → 得在
beforeUnmount(趁还没走) - 想监听数据变化 → 得在
updated之前(避免死循环)
写错时机,要么拿不到东西,要么报错,要么内存泄漏。
二、生命周期的四个阶段
Vue 组件的一生可以分成四个阶段,每个阶段有一个"前"一个"后"两个钩子。
| 阶段 | Vue 3 钩子 | Vue 2 钩子 | 干什么 |
|---|---|---|---|
| 创建 | beforeCreate / created | beforeCreate / created | 初始化数据、观测 |
| 挂载 | beforeMount / mounted | beforeMount / mounted | 编译模板、挂载 DOM |
| 更新 | beforeUpdate / updated | beforeUpdate / updated | 数据变化时重新渲染 |
| 卸载 | beforeUnmount / unmounted | beforeDestroy / destroyed | 销毁实例、清理收尾 |
下面逐个阶段讲。本文以 Vue 3 为主,Vue 2 的差异在第十节统一对比。
三、创建阶段:beforeCreate & created
3.1 beforeCreate
实例刚被创建,但是数据观测(data)和事件配置都还没开始 。这时候 data、computed、methods 全都访问不到。
javascript
export default {
data() {
return { msg: "hello" };
},
beforeCreate() {
console.log(this.msg); // undefined ← data 还没挂载
console.log(this.sayHi); // undefined ← methods 也没好
},
methods: {
sayHi() { console.log("hi"); }
}
};
用途:几乎不用。想做"最早的全局配置"才碰它(比如改全局配置项),日常开发基本碰不到。
3.2 created
实例创建完成,data、computed、methods 都已经就位 ,但还没开始编译模板,DOM 还不存在。
javascript
export default {
data() {
return { userList: [] };
},
async created() {
// 数据已经能用,可以发请求
const res = await fetch("/api/users");
this.userList = await res.json(); // ← 能正常赋值
console.log(this.$el); // undefined ← 还没挂载,拿不到根节点
}
};
用途 :发请求拿数据的最佳时机之一。因为这时 data 已经能用,且比 mounted 更早,数据可以更早返回、更早渲染。SSR 场景下也只有 created 能用(mounted 在服务端不执行)。
3.3 注解
1.beforeCreate 和 created 的分界线
分界线就是"有没有完成 data 的响应式初始化 "。beforeCreate 之前 data 还是 undefined,之后(created)就能 this.msg 访问了。这个分界由 Vue 内部的 initState() 完成,它会依次处理 props、methods、data、computed、watch。
2.为什么 created 不能操作 DOM
因为模板还没编译,DOM 还没生成。this.$el 此时是 undefined,document.querySelector 也找不到组件内部的元素。要操作 DOM 必须等 mounted。
四、挂载阶段:beforeMount & mounted
4.1 beforeMount
模板已经编译成渲染函数,马上要把虚拟 DOM 转成真实 DOM 挂到页面上,但还没挂,页面还是旧的。
javascript
export default {
data() { return { count: 0 }; },
beforeMount() {
console.log(document.getElementById("app").innerHTML);
// 还是旧内容 ← 还没替换
console.log(this.$el); // undefined(Vue3) / 即将挂载的元素(Vue2)
}
};
用途:基本不用。极少情况下需要在挂载前做最后一道处理,比如改一下要渲染的数据(但通常直接改 data 就够了)。
4.2 mounted
真实 DOM 已经挂到页面上 ,this.$el、document.querySelector 都能用了。这是组件生命周期里第一个能操作 DOM 的时机。
javascript
export default {
data() { return { chart: null }; },
mounted() {
// DOM 已经存在,可以初始化第三方库
const canvas = document.getElementById("myCanvas");
this.chart = new Chart(canvas, { /* 配置 */ });
// 可以拿到实际尺寸了
const rect = canvas.getBoundingClientRect();
console.log(rect.width); // 真实像素值 ← mounted 才测得准
}
};
用途:
- 初始化依赖 DOM 的第三方库(ECharts、Swiper、CodeMirror 等)
- 监听 window/document 事件(resize、scroll、keydown)
- 启动定时器、WebSocket 连接
- 拿元素尺寸(getBoundingClientRect)
4.3 注解
1.mounted 不代表所有子组件都挂载完成
在 Vue 3 中,父组件的 mounted 保证自身 DOM 已挂载 ,但不保证所有子组件都已挂载完成 (子组件可能是异步的)。如果需要等整个树都准备好,可以用 this.$nextTick 或在子组件挂载后用 emits 通知父组件。
2.mounted 只执行一次
和 created 一样,正常情况下 mounted 只触发一次。后续数据变化只会触发更新阶段的钩子,不会再走 created/mounted。除非组件被 keep-alive 缓存后再次激活,那走的是 activated 而不是 mounted。
五、更新阶段:beforeUpdate & updated
5.1 beforeUpdate
数据变了,但 DOM 还没重新渲染 。这时 this.xxx 是新值,但页面上的还是旧值。
javascript
export default {
data() { return { count: 0 }; },
beforeUpdate() {
console.log(this.count); // 新值,比如 1 ← 数据已经更新
console.log(this.$el.textContent); // 旧值 0 ← DOM 还没同步
}
};
用途 :在 DOM 重新渲染前最后改一次状态的好时机。比如某个数据变了,你需要在更新前同步更新另一个依赖它的数据,避免触发两次重渲染。
5.2 updated
数据变了,DOM 已经重新渲染完毕。这时数据和页面是同步的。
javascript
export default {
data() { return { list: [] }; },
updated() {
// 列表更新后,DOM 也更新了,可以拿到新增的元素
const lastItem = document.querySelector(".item:last-child");
console.log(lastItem); // 新渲染的节点 ← 这时才能拿到
}
};
用途:
- 拿数据更新后的最新 DOM(比如列表渲染完后做滚动定位)
- 依赖 DOM 尺寸的二次计算(如瀑布流重排)
5.3 注解
1.updated 里千万别直接改数据,会死循环
在 updated 里改 data,会让数据又变,又触发 update,又执行 updated......形成无限递归,栈溢出或卡死。如果一定要改,必须加条件:
javascript
updated() {
if (this.needFix) {
this.needFix = false; // ← 改完立刻把自己关掉
this.someValue = "fixed";
}
}
更好的做法是用 watch + this.$nextTick,而不是在 updated 里改。
2.updated 是组件任意数据变化都会触发
不是只有你看的那个数据变才触发,组件内任何响应式数据变了都会触发 updated。所以 updated 会被频繁调用,在里面干重活代价很大。
六、卸载阶段:beforeUnmount & unmounted
6.1 beforeUnmount
组件马上要被销毁,但此刻实例还完全可用,data、methods、DOM 都还在。
javascript
export default {
data() { return { timer: null }; },
beforeUnmount() {
// 还能访问 this,适合做清理
clearInterval(this.timer);
window.removeEventListener("resize", this.handleResize);
this.socket?.close();
}
};
用途 :清理工作的主战场。在这里取消定时器、移除事件监听、关闭 WebSocket、取消 axios 请求、销毁第三方库实例。
6.2 unmounted
组件已经销毁,所有指令解绑、事件监听器移除、子实例卸载完毕。这时再操作 this 已经没意义了。
javascript
export default {
unmounted() {
console.log("走好"); ← 最后道别,通常不再做重活
}
};
用途:基本不用。该清理的活儿在 beforeUnmount 就干完了,unmounted 更像是个"墓志铭"钩子。
6.3 注解
1.为什么清理要放 beforeUnmount 而不是 unmounted
因为 beforeUnmount 时 this 还完整,定时器 id、监听器引用都还能拿到。到 unmounted 时很多内部引用可能已经被 Vue 置空,再清理就麻烦了。记住一个口诀:清理在 before,告别在 after。
2.路由切换不一定触发 unmount
如果组件被 <keep-alive> 包裹,路由切走时调用的是 deactivated 而不是 unmounted,组件被缓存,下次回来调用 activated。只有真正离开 keep-alive 缓存范围才会 unmount。
七、特殊钩子
除了四大阶段,Vue 还提供了一些特殊场景的钩子。
7.1 keep-alive 专属:activated & deactivated
<keep-alive> 包裹的组件被缓存,再次激活时不会重新走 created/mounted,而是触发这两个钩子。
javascript
export default {
activated() {
console.log("从缓存中唤醒"); ← 适合恢复定时器、开始轮询
},
deactivated() {
console.log("被缓存(休眠)"); ← 适合暂停定时器、停止轮询
}
};
| 状态 | 普通组件 | keep-alive 组件 |
|---|---|---|
| 首次进入 | created → mounted | created → mounted → activated |
| 离开 | beforeUnmount → unmounted | deactivated |
| 再次进入 | created → mounted | activated(不走 created) |
| 真正销毁 | - | beforeUnmount → unmounted |
7.2 错误捕获:errorCaptured
后代组件抛出错误时触发,离错误最近的父级先收到,然后错误会沿父链向上抛。
javascript
export default {
errorCaptured(err, instance, info) {
console.log("子组件炸了:", err.message);
console.log("在哪个组件:", instance);
console.log("错误来源:", info);
return false; // ← 返回 false 阻止错误继续向上传播
}
};
用途:做组件级错误兜底,比如显示一个 fallback UI,而不是整个页面崩掉。
7.3 调试专用:renderTracked & renderTriggered(Vue 3)
只在开发环境生效,用来调试哪个响应式数据触发了一次重新渲染。
javascript
export default {
renderTriggered(event) {
console.log("谁触发了我:", event); ← { effect, target, key, type }
}
};
线上不要用,生产环境会被自动移除。
八、父子组件生命周期的执行顺序
这是面试和实战都很常考的点。记住一句话:创建是父先,挂载是子先。
8.1 挂载阶段
父 beforeCreate
父 created
父 beforeMount
子 beforeCreate
子 created
子 beforeMount
子 mounted
父 mounted ← 父等子挂完才 mounted
为什么这样:父组件挂载前需要先把子组件渲染好,所以父 beforeMount 之后会去走子的流程,等所有子组件都 mounted 了,父才 mounted。
8.2 更新阶段
父 beforeUpdate
子 beforeUpdate
子 updated
父 updated
数据从父流向子,所以父先知道要更新,再通知子,子更新完父才收尾。
8.3 卸载阶段
父 beforeUnmount
子 beforeUnmount
子 unmounted
父 unmounted
父要走了,先把子送走,自己再走。
8.4 代码验证
javascript
// 父组件
export default {
beforeMount() { console.log("父 beforeMount"); },
mounted() { console.log("父 mounted"); }
}
// 子组件
export default {
beforeMount() { console.log(" 子 beforeMount"); },
mounted() { console.log(" 子 mounted"); }
}
// 控制台输出:
// 父 beforeMount
// 子 beforeMount
// 子 mounted
// 父 mounted
九、常见使用场景速查
| 场景 | 推荐钩子 | 原因 |
|---|---|---|
| 发请求拿数据 | created | 数据已就位,比 mounted 更早 |
| 操作 DOM / 初始化第三方库 | mounted | DOM 才存在 |
| 监听 window 事件、启动定时器 | mounted | 需要在 beforeUnmount 配套清理 |
| 清理定时器 / 移除监听 / 关连接 | beforeUnmount | this 还能用,引用还在 |
| 数据变化后做 DOM 二次操作 | updated / watch+nextTick | DOM 已同步 |
| keep-alive 缓存组件恢复轮询 | activated / deactivated | 不走 created/mounted |
| 全局错误兜底 | errorCaptured | 捕获后代组件错误 |
| 调试响应式触发来源 | renderTriggered | 仅开发环境 |