在前几篇文章中,我们已经学习了Vue的核心概念、指令语法、事件处理、响应式原理等知识。今天我们将深入Vue的生命周期,理解一个Vue实例从创建到销毁的完整过程。掌握生命周期,你就能在正确的时机做正确的事情。
一、什么是生命周期?
生命周期:一个事物从出生到最终的死亡,整个经历的过程叫做生命周期。
生活中的生命周期
以人的生命周期为例:
- 出生:打疫苗
- 3岁:上幼儿园
- 6岁:上小学
- 12岁:上初中
- 15岁:上高中
- 60岁:退休
- 临终:立遗嘱
- 死亡:火化
可以看到,在这个生命线上有很多不同的时间节点,在每个时间节点上我们去做不同的事情。
Vue的生命周期
Vue的生命周期指的是:vm对象从创建到最终销毁的整个过程。
在这个过程中,Vue会在不同的时间节点自动调用不同的函数,这些函数被称为生命周期钩子函数。
虚拟DOM在内存中就绪时 → 调用a函数
虚拟DOM转换成真实DOM渲染到页面时 → 调用b函数
Vue的data发生改变时 → 调用c函数
Vue实例被销毁时 → 调用x函数
关键特点:这些钩子函数不需要程序员手动调用,由Vue自动调用。程序员只需按照需求编写函数体,到了那个时间点自动就会执行。
二、掌握Vue生命周期有什么用?
研究Vue的生命周期主要是研究:在不同的时刻Vue做了哪些不同的事情。
例如:在vm被销毁之前,需要将绑定到元素上的自定义事件全部解绑。那么这个解绑的代码就需要找到一个合适的地方来写------可以写到 beforeDestroy() 这个函数中。
核心目标:研究在哪个时刻调用了哪个钩子函数,以及在该钩子函数中可以做什么事情。
三、Vue生命周期的4个阶段8个钩子
Vue的生命周期可以被划分为4个阶段,每个阶段调用两个钩子函数:
| 阶段 | 钩子函数 | 调用时机 |
|---|---|---|
| 初始阶段 | beforeCreate() |
创建前 |
created() |
创建后 | |
| 挂载阶段 | beforeMount() |
挂载前 |
mounted() |
挂载后 | |
| 更新阶段 | beforeUpdate() |
更新前 |
updated() |
更新后 | |
| 销毁阶段 | beforeDestroy() |
销毁前 |
destroyed() |
销毁后 |
命名规律 :每个阶段的钩子函数命名遵循
beforeXxx()和xxxed()的规律。
钩子函数写在哪里?
直接写在Vue构造函数的 options 对象当中,与 data、methods 平级。
const vm = new Vue({
el: "#app",
data: {
msg: "Vue生命周期",
counter: 1
},
methods: {
add() {
console.log("add....");
this.counter++;
},
destroy() {
this.$destroy();
}
},
watch: {
counter() {
console.log("counter被监视了一次!");
}
},
// 生命周期钩子函数
beforeCreate() {
console.log("beforeCreate");
},
created() {
console.log("created");
},
beforeMount() {
console.log("beforeMount");
},
mounted() {
console.log("mounted");
},
beforeUpdate() {
console.log("beforeUpdate");
},
updated() {
console.log("updated");
},
beforeDestroy() {
console.log("beforeDestroy");
},
destroyed() {
console.log("destroyed");
}
});
四、初始阶段详解
4.1 执行流程
- 创建Vue实例
vm(生命的起点) - 初始化事件对象和生命周期
- 调用
beforeCreate()钩子函数 - 初始化数据代理和数据监测
- 调用
created()钩子函数 - 编译模板语句生成虚拟DOM
4.2 关于el和template的判断逻辑
el是否存在?
├── el存在
│ ├── template是否存在?
│ │ ├── template存在 → 编译template模板语句
│ │ └── template不存在 → 编译el模板语句
│ └──
└── el不存在
└── 等待手动调用 vm.$mount(el) → 回到上面继续判断
结论:
- 流程要想继续,
el必须存在 el和template同时存在,优先选择template- 如果没有
template,则编译el模板语句
4.3 钩子函数详解
beforeCreate() --- 创建前
此时数据代理和数据监测尚未创建完毕,无法访问 data 中的数据和 methods 中的方法。
beforeCreate() {
console.log("beforeCreate", this.counter); // undefined
// this.add(); // ❌ 报错:不存在
}
created() --- 创建后
数据代理和数据监测创建完毕,可以访问 data 中的数据和 methods 中的方法。
created() {
console.log("created", this.counter); // 1
this.add(); // ✅ 可以调用
}
4.4 该阶段适合做什么?
| 钩子函数 | 适合做的事情 |
|---|---|
beforeCreate |
添加loading效果;自定义属性放到this身上(如全局事件总线) |
created |
结束loading效果;发送网络请求获取数据;添加定时器 |
五、挂载阶段详解
5.1 执行流程
- 调用
beforeMount()钩子函数 - 给vm追加
$el属性,代替el,$el代表真实的DOM元素 - 调用
mounted()钩子函数
5.2 钩子函数详解
beforeMount() --- 挂载前
此时页面还未渲染,真实DOM还未生成。此时操作数据,不会最终显示在页面中。
beforeMount() {
console.log("beforeMount");
// debugger // 可以用断点查看此时页面状态
}
mounted() --- 挂载后
真实DOM生成,页面渲染完成,可以操作页面的DOM元素了。
mounted() {
console.log("mounted");
// 可以获取真实DOM元素
console.log(this.$el);
console.log(this.$el instanceof HTMLElement); // true
}
5.3 该阶段适合做什么?
| 钩子函数 | 适合做的事情 |
|---|---|
mounted |
操作页面的DOM元素;请求数据 |
5.4 面试题:请求数据应该在哪个钩子里?
问题:请求数据是在哪个钩子里?原因是什么?
答案 :请求数据可以在 created 或 mounted 中发起,但最好在 created 中。
原因 :mounted 已经挂载完毕,数据请求回来后需要重新渲染页面,可能会出现闪屏的情况。而在 created 中发起请求,数据回来得更早,可以避免不必要的DOM操作。
六、更新阶段详解
6.1 执行流程
data发生变化(这是该阶段开始的标志)- 调用
beforeUpdate()钩子函数 - 虚拟DOM重新渲染和修补
- 调用
updated()钩子函数
6.2 钩子函数详解
beforeUpdate() --- 更新前
此时只是内存中的数据发生变化,页面还未更新。
beforeUpdate() {
console.log("beforeUpdate");
}
updated() --- 更新后
页面已经更新完成。
updated() {
console.log("updated");
}
6.3 该阶段适合做什么?
| 钩子函数 | 适合做的事情 |
|---|---|
beforeUpdate |
更新之前访问现有DOM;手动移除已添加的事件监听器 |
updated |
页面更新后,对数据做统一处理 |
七、销毁阶段详解
7.1 执行流程
vm.$destroy()方法被调用(这是该阶段开始的标志)- 调用
beforeDestroy()钩子函数 - 卸载子组件和监视器、解绑自定义事件监听器
- 调用
destroyed()钩子函数
7.2 钩子函数详解
beforeDestroy() --- 销毁前
此时Vue实例还在,虽然vm上的监视器、子组件、自定义事件监听器还在,但它们已经都不能用了。此时修改data也不会重新渲染页面了。
beforeDestroy() {
console.log("beforeDestroy");
console.log(this); // vm实例还在
this.counter = 1000; // 修改数据,但页面不会更新
}
destroyed() --- 销毁后
虽然翻译为"已销毁",但此时Vue实例还在,空间并没有释放,只不过马上要释放了。"已销毁"指的是vm对象上所有的东西都已经解绑完成了。
destroyed() {
console.log("destroyed");
console.log(this); // vm实例还在,但功能已失效
}
7.3 该阶段适合做什么?
| 钩子函数 | 适合做的事情 |
|---|---|
beforeDestroy |
销毁前的准备工作,如同人临终前写遗嘱。清除定时器、解绑自定义事件、解除watch监听等 |
八、生命周期完整流程图
new Vue()
↓
初始化事件和生命周期
↓
beforeCreate() ← 数据代理/监测尚未创建
↓
初始化数据代理和数据监测
↓
created() ← 可以访问data和methods了
↓
判断是否有el选项
↓
├── 有el → 判断是否有template
│ ├── 有template → 编译template
│ └── 无template → 编译el外部HTML
│
└── 无el → 等待vm.$mount(el)调用
↓
(回到上面继续)
↓
beforeMount() ← 页面尚未渲染
↓
创建vm.$el并替换el
↓
mounted() ← 页面已渲染,可以操作DOM
↓
(等待数据变化)
↓
数据发生变化
↓
beforeUpdate() ← 数据已变,页面未更新
↓
虚拟DOM重新渲染和修补
↓
updated() ← 页面已更新
↓
(等待vm.$destroy()调用)
↓
vm.$destroy()被调用
↓
beforeDestroy() ← 清理工作(清除定时器、解绑事件等)
↓
卸载子组件、监视器、自定义事件
↓
destroyed() ← 实例功能已失效
九、各阶段总结
| 阶段 | 钩子函数 | 特点 | 适合做什么 |
|---|---|---|---|
| 初始阶段 | beforeCreate |
无法访问data和methods | 添加loading、全局事件总线 |
created |
可访问data和methods | 请求数据、结束loading、添加定时器 | |
| 挂载阶段 | beforeMount |
页面未渲染 | 最后的DOM操作准备 |
mounted |
页面已渲染 | 操作DOM、请求数据 | |
| 更新阶段 | beforeUpdate |
数据已变,页面未更新 | 访问旧DOM、移除事件监听 |
updated |
页面已更新 | 数据统一处理 | |
| 销毁阶段 | beforeDestroy |
实例仍在,但功能即将失效 | 清除定时器、解绑事件、解除watch |
destroyed |
实例功能已解绑 | 最终清理 |
十、最佳实践
10.1 请求数据的最佳位置
// ✅ 推荐:在created中请求数据
created() {
axios.get('/api/data').then(res => {
this.list = res.data;
});
}
// 可选:在mounted中请求数据
mounted() {
axios.get('/api/data').then(res => {
this.list = res.data;
});
}
10.2 清理资源的最佳位置
data() {
return {
timer: null
};
},
created() {
this.timer = setInterval(() => {
console.log('定时器执行中...');
}, 1000);
},
beforeDestroy() {
// ✅ 清除定时器
clearInterval(this.timer);
this.timer = null;
// ✅ 解绑自定义事件
// this.$off('customEvent');
// ✅ 解除watch监听
// this.$watch('xxx', () => {});
}
10.3 操作DOM的最佳位置
mounted() {
// ✅ 此时可以安全地操作DOM
this.$refs.myDiv.style.color = 'red';
// 或者使用nextTick确保DOM更新完成
this.$nextTick(() => {
// DOM已更新
});
}
掌握了生命周期,你就能在正确的时机做正确的事情,写出更加健壮和高效的Vue应用。下一篇文章我们将继续学习组件化开发,敬请期待!