Vue 生命周期钩子详解
前言
Vue 组件的生命周期钩子是 Vue.js 框架的核心特性之一,它允许开发者在组件的不同阶段执行特定的逻辑。理解生命周期钩子对于编写高效、可维护的 Vue 应用至关重要。本文将详细讲解 Vue 2 和 Vue 3 的生命周期钩子,包括执行顺序、使用场景和常见问题。
一、生命周期概述
1.1 生命周期阶段
Vue 组件的生命周期分为三个主要阶段:
| 阶段 | 说明 | 包含钩子 |
|---|---|---|
| 创建阶段 | 组件实例创建到挂载到 DOM | beforeCreate, created, beforeMount, mounted |
| 更新阶段 | 响应式数据变化导致重新渲染 | beforeUpdate, updated |
| 销毁阶段 | 组件实例从 DOM 移除并销毁 | beforeDestroy, destroyed |
1.2 Vue 2 生命周期图示
┌─────────────────────────────────────────────────────────────┐
│ Vue 实例创建 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌────────────────┐
│ beforeCreate │ ← 实例创建前,数据未初始化
└────────────────┘
│
▼
┌────────────────┐
│ created │ ← 实例创建完成,可访问 data、methods
└────────────────┘
│
▼
┌────────────────┐
│ beforeMount │ ← DOM 挂载前,$el 未生成
└────────────────┘
│
▼
┌──────────────────────────────┐
│ 渲染模板 → 生成虚拟 DOM │
│ → 创建真实 DOM │
└──────────────────────────────┘
│
▼
┌────────────────┐
│ mounted │ ← DOM 挂载完成,可操作 DOM
└────────────────┘
│
▼
┌──────────────────────────────────────┐
│ 数据变化触发更新 │
└──────────────────────────────────────┘
│
▼
┌────────────────┐
│ beforeUpdate │ ← 更新前,数据已变 DOM 未更
└────────────────┘
│
▼
┌──────────────────────────────┐
│ 重新渲染模板 → 更新真实 DOM │
└──────────────────────────────┘
│
▼
┌────────────────┐
│ updated │ ← 更新完成,DOM 已更新
└────────────────┘
│
▼
┌──────────────────────────────────────┐
│ 组件销毁触发 │
└──────────────────────────────────────┘
│
▼
┌────────────────┐
│ beforeDestroy │ ← 销毁前,实例仍可访问
└────────────────┘
│
▼
┌──────────────────────────────┐
│ 移除事件监听、清理定时器等 │
└──────────────────────────────┘
│
▼
┌────────────────┐
│ destroyed │ ← 销毁完成,实例被销毁
└────────────────┘1.3 Vue 2 vs Vue 3 生命周期钩子对比
| Vue 2 钩子 | Vue 3 选项式 API | Vue 3 组合式 API | 说明 |
|---|---|---|---|
| beforeCreate | beforeCreate | setup() | 实例创建前 |
| created | created | setup() | 实例创建完成 |
| beforeMount | beforeMount | onBeforeMount | DOM 挂载前 |
| mounted | mounted | onMounted | DOM 挂载完成 |
| beforeUpdate | beforeUpdate | onBeforeUpdate | 更新前 |
| updated | updated | onUpdated | 更新完成 |
| beforeDestroy | beforeUnmount | onBeforeUnmount | 销毁前 |
| destroyed | unmounted | onUnmounted | 销毁完成 |
| activated | activated | onActivated | keep-alive 激活 |
| deactivated | deactivated | onDeactivated | keep-alive 失活 |
| errorCaptured | errorCaptured | onErrorCaptured | 错误捕获 |
二、Vue 2 选项式 API 生命周期详解
2.1 创建阶段
beforeCreate
javascript
export default {
beforeCreate() {
console.log("beforeCreate: 实例创建前");
console.log("data:", this.$data); // undefined
console.log("el:", this.$el); // undefined
console.log("methods:", this.$options.methods); // 可访问
},
};特点:
- 实例刚创建,数据观测和初始化尚未开始
- 无法访问
data、computed、methods - 可用于初始化非响应式数据
created
javascript
export default {
data() {
return {
message: "Hello Vue",
};
},
created() {
console.log("created: 实例创建完成");
console.log("data:", this.$data); // { message: 'Hello Vue' }
console.log("message:", this.message); // 'Hello Vue'
console.log("el:", this.$el); // undefined
// 适合发起异步请求
this.fetchData();
},
methods: {
fetchData() {
// 发起 API 请求
},
},
};特点:
- 实例创建完成,数据已初始化
- 可访问
data、computed、methods $el尚未生成,无法操作 DOM- 最佳实践:发起异步请求、初始化状态
2.2 挂载阶段
beforeMount
javascript
export default {
beforeMount() {
console.log("beforeMount: DOM 挂载前");
console.log("el:", this.$el); // 已生成但未挂载
console.log("document.body:", document.body.contains(this.$el)); // false
},
};特点:
- 模板已编译,
$el已生成 - DOM 尚未挂载到页面
- 可用于修改
$el但不推荐
mounted
javascript
export default {
mounted() {
console.log("mounted: DOM 挂载完成");
console.log("el:", this.$el); // 已挂载到 DOM
console.log("document.body:", document.body.contains(this.$el)); // true
// 操作 DOM
this.$el.style.color = "red";
// 初始化第三方库
this.initChart();
},
methods: {
initChart() {
// 初始化图表库
},
},
};特点:
- DOM 已挂载到页面
- 可访问和操作 DOM
- 最佳实践:初始化第三方库、DOM 操作、添加事件监听
2.3 更新阶段
beforeUpdate
javascript
export default {
data() {
return {
count: 0,
};
},
beforeUpdate() {
console.log("beforeUpdate: 更新前");
console.log("count:", this.count); // 更新后的值
console.log("DOM count:", this.$el.textContent); // 更新前的 DOM 值
},
methods: {
increment() {
this.count++;
},
},
};特点:
- 响应式数据已更新
- DOM 尚未重新渲染
- 可访问更新后的状态和更新前的 DOM
- 避免在此时修改数据(会导致无限循环)
updated
javascript
export default {
data() {
return {
items: [],
};
},
updated() {
console.log("updated: 更新完成");
console.log("DOM updated:", this.$el.textContent); // 更新后的 DOM 值
// 响应 DOM 更新
this.scrollToBottom();
},
methods: {
scrollToBottom() {
// 滚动到底部
},
},
};特点:
- DOM 已重新渲染完成
- 可访问更新后的 DOM
- 最佳实践:响应 DOM 更新、调整滚动位置
2.4 销毁阶段
beforeDestroy
javascript
export default {
data() {
return {
timer: null,
};
},
mounted() {
this.timer = setInterval(() => {
console.log("tick");
}, 1000);
},
beforeDestroy() {
console.log("beforeDestroy: 销毁前");
console.log("timer:", this.timer); // 仍可访问
// 清理定时器
clearInterval(this.timer);
// 移除事件监听
window.removeEventListener("resize", this.handleResize);
},
methods: {
handleResize() {
// 处理窗口大小变化
},
},
};特点:
- 实例即将被销毁
- 仍可访问所有属性和方法
- 最佳实践:清理定时器、移除事件监听、取消订阅
destroyed
javascript
export default {
destroyed() {
console.log("destroyed: 销毁完成");
console.log("instance:", this); // 实例已销毁
// 所有事件监听已自动移除
// 所有子组件已销毁
},
};特点:
- 实例已完全销毁
- 所有事件监听已自动移除
- 所有子组件已销毁
三、Vue 3 组合式 API 生命周期详解
3.1 setup 函数
javascript
import { ref, onMounted, onUnmounted } from "vue";
export default {
setup() {
const count = ref(0);
// setup 函数等效于 beforeCreate + created
console.log("setup: 等同于 beforeCreate + created");
console.log("count:", count.value); // 可访问
// 注意:setup 中没有 this
// 需要通过参数获取 props 和 context
return {
count,
};
},
};特点:
setup在beforeCreate之前执行- 没有
this,需要通过参数获取上下文 - 可访问响应式数据和方法
- 最佳实践:初始化响应式数据、注册生命周期钩子
3.2 组合式 API 生命周期钩子
javascript
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
} from "vue";
export default {
setup() {
const message = ref("Hello Vue 3");
// DOM 挂载前
onBeforeMount(() => {
console.log("onBeforeMount: DOM 挂载前");
});
// DOM 挂载完成
onMounted(() => {
console.log("onMounted: DOM 挂载完成");
// 操作 DOM
});
// 更新前
onBeforeUpdate(() => {
console.log("onBeforeUpdate: 更新前");
});
// 更新完成
onUpdated(() => {
console.log("onUpdated: 更新完成");
});
// 销毁前
onBeforeUnmount(() => {
console.log("onBeforeUnmount: 销毁前");
// 清理资源
});
// 销毁完成
onUnmounted(() => {
console.log("onUnmounted: 销毁完成");
});
return {
message,
};
},
};3.3 组合式 API 实践案例
javascript
import { ref, onMounted, onUnmounted } from "vue";
export default {
setup() {
const windowWidth = ref(window.innerWidth);
const windowHeight = ref(window.innerHeight);
const handleResize = () => {
windowWidth.value = window.innerWidth;
windowHeight.value = window.innerHeight;
};
onMounted(() => {
window.addEventListener("resize", handleResize);
console.log("Mounted: 添加 resize 监听");
});
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
console.log("Unmounted: 移除 resize 监听");
});
return {
windowWidth,
windowHeight,
};
},
};四、父子组件生命周期顺序
4.1 创建阶段顺序
父组件 beforeCreate
父组件 created
父组件 beforeMount
└── 子组件 beforeCreate
└── 子组件 created
└── 子组件 beforeMount
└── 子组件 mounted
父组件 mounted4.2 更新阶段顺序
父组件 beforeUpdate
└── 子组件 beforeUpdate
└── 子组件 updated
父组件 updated4.3 销毁阶段顺序
父组件 beforeDestroy
└── 子组件 beforeDestroy
└── 子组件 destroyed
父组件 destroyed4.4 代码验证
javascript
// 父组件
export default {
beforeCreate() { console.log('Parent: beforeCreate') },
created() { console.log('Parent: created') },
beforeMount() { console.log('Parent: beforeMount') },
mounted() { console.log('Parent: mounted') },
beforeUpdate() { console.log('Parent: beforeUpdate') },
updated() { console.log('Parent: updated') },
beforeDestroy() { console.log('Parent: beforeDestroy') },
destroyed() { console.log('Parent: destroyed') }
}
// 子组件
export default {
beforeCreate() { console.log('Child: beforeCreate') },
created() { console.log('Child: created') },
beforeMount() { console.log('Child: beforeMount') },
mounted() { console.log('Child: mounted') },
beforeUpdate() { console.log('Child: beforeUpdate') },
updated() { console.log('Child: updated') },
beforeDestroy() { console.log('Child: beforeDestroy') },
destroyed() { console.log('Child: destroyed') }
}五、keep-alive 相关生命周期
5.1 activated 和 deactivated
javascript
// Vue 2 选项式 API
export default {
activated() {
console.log('activated: 组件被激活')
// 恢复状态
this.resumeData()
},
deactivated() {
console.log('deactivated: 组件被失活')
// 保存状态
this.saveData()
}
}
// Vue 3 组合式 API
import { onActivated, onDeactivated } from 'vue'
export default {
setup() {
onActivated(() => {
console.log('onActivated: 组件被激活')
})
onDeactivated(() => {
console.log('onDeactivated: 组件被失活')
})
}
}5.2 keep-alive 使用场景
vue
<template>
<div>
<keep-alive>
<component :is="currentComponent"></component>
</keep-alive>
</div>
</template>
<script>
export default {
data() {
return {
currentComponent: "ComponentA",
scrollPosition: 0,
};
},
components: {
ComponentA,
ComponentB,
},
activated() {
// 恢复滚动位置
window.scrollTo(0, this.scrollPosition);
},
deactivated() {
// 保存滚动位置
this.scrollPosition = window.scrollY;
},
};
</script>六、常见踩坑与问题排查
6.1 在 beforeMount 中操作 DOM
javascript
// ❌ 错误:beforeMount 中无法访问已挂载的 DOM
export default {
beforeMount() {
// $el 已生成但尚未挂载到页面
this.$el.querySelector('.my-class').style.color = 'red' // 可能报错
}
}
// ✅ 正确:在 mounted 中操作 DOM
export default {
mounted() {
this.$el.querySelector('.my-class').style.color = 'red' // 安全
}
}6.2 在 updated 中修改数据导致无限循环
javascript
// ❌ 错误:在 updated 中修改数据会导致无限循环
export default {
data() {
return { count: 0 }
},
updated() {
this.count++ // 触发更新 → updated → this.count++ → 无限循环
}
}
// ✅ 正确:使用条件判断或 watch
export default {
data() {
return { count: 0, targetCount: 10 }
},
watch: {
count(newVal) {
if (newVal < this.targetCount) {
this.count++
}
}
}
}6.3 忘记清理定时器和事件监听
javascript
// ❌ 错误:忘记清理定时器,组件销毁后仍在运行
export default {
mounted() {
this.timer = setInterval(() => {
console.log('tick')
}, 1000)
}
}
// ✅ 正确:在 beforeDestroy 中清理
export default {
data() {
return { timer: null }
},
mounted() {
this.timer = setInterval(() => {
console.log('tick')
}, 1000)
},
beforeDestroy() {
clearInterval(this.timer)
}
}6.4 异步请求在组件销毁后返回
javascript
// ❌ 错误:组件销毁后,异步请求返回导致报错
export default {
mounted() {
this.fetchData()
},
methods: {
async fetchData() {
const result = await api.getData()
this.data = result // 组件销毁后执行会报错
}
}
}
// ✅ 正确:使用取消令牌或标志位
export default {
data() {
return {
data: null,
isMounted: false
}
},
mounted() {
this.isMounted = true
this.fetchData()
},
beforeDestroy() {
this.isMounted = false
},
methods: {
async fetchData() {
const result = await api.getData()
if (this.isMounted) {
this.data = result
}
}
}
}6.5 Vue 3 中使用 this
javascript
// ❌ 错误:setup 中没有 this
export default {
setup() {
this.message = 'Hello' // 错误!this 是 undefined
}
}
// ✅ 正确:使用响应式数据
import { ref } from 'vue'
export default {
setup() {
const message = ref('Hello')
return { message }
}
}七、优化方案与进阶拓展
7.1 使用 watch 替代 updated
javascript
// ❌ 不推荐:updated 会在每次更新时触发
export default {
updated() {
if (this.someData) {
this.doSomething()
}
}
}
// ✅ 推荐:使用 watch 精确监听
export default {
watch: {
someData(newVal) {
if (newVal) {
this.doSomething()
}
}
}
}
// Vue 3 组合式 API
import { watch } from 'vue'
export default {
setup() {
const someData = ref(null)
watch(someData, (newVal) => {
if (newVal) {
doSomething()
}
})
}
}7.2 使用 nextTick 确保 DOM 更新
javascript
// 场景:修改数据后立即操作 DOM
export default {
data() {
return { message: 'Hello' }
},
methods: {
updateMessage() {
this.message = 'Updated'
// DOM 尚未更新
console.log(this.$el.textContent) // 'Hello'
// 使用 nextTick 等待 DOM 更新
this.$nextTick(() => {
console.log(this.$el.textContent) // 'Updated'
})
}
}
}
// Vue 3
import { nextTick } from 'vue'
export default {
setup() {
const message = ref('Hello')
const updateMessage = async () => {
message.value = 'Updated'
// DOM 尚未更新
await nextTick()
// DOM 已更新
}
}
}7.3 使用 mixin 复用生命周期逻辑
javascript
// 创建可复用的生命周期 mixin
const resizeMixin = {
data() {
return {
windowWidth: window.innerWidth,
windowHeight: window.innerHeight,
};
},
mounted() {
window.addEventListener("resize", this.handleResize);
},
beforeDestroy() {
window.removeEventListener("resize", this.handleResize);
},
methods: {
handleResize() {
this.windowWidth = window.innerWidth;
this.windowHeight = window.innerHeight;
},
},
};
// 在组件中使用
export default {
mixins: [resizeMixin],
mounted() {
// 调用父级 mounted
this.$options.mounted.forEach((hook) => hook.call(this));
// 组件自身逻辑
console.log("Component mounted");
},
};7.4 Vue 3 组合式 API 封装
javascript
// 封装可复用的逻辑
import { ref, onMounted, onUnmounted } from "vue";
export function useWindowSize() {
const width = ref(window.innerWidth);
const height = ref(window.innerHeight);
const handleResize = () => {
width.value = window.innerWidth;
height.value = window.innerHeight;
};
onMounted(() => {
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
});
return {
width,
height,
};
}
// 在组件中使用
import { useWindowSize } from "../composables";
export default {
setup() {
const { width, height } = useWindowSize();
return {
width,
height,
};
},
};八、全文总结
Vue 生命周期钩子是理解 Vue 组件工作原理的关键:
- 创建阶段:
beforeCreate→created→beforeMount→mounted - 更新阶段:
beforeUpdate→updated - 销毁阶段:
beforeDestroy→destroyed - Vue 3 变化:
setup替代beforeCreate和created,其他钩子通过onXxx函数注册 - 父子顺序:父组件先创建、后销毁;子组件后创建、先销毁
- 最佳实践:
- 在
created中发起异步请求 - 在
mounted中操作 DOM 和初始化第三方库 - 在
beforeDestroy中清理资源(定时器、事件监听) - 使用
nextTick确保 DOM 更新后再操作 - Vue 3 使用组合式 API 封装可复用逻辑
- 在
