从创建到销毁,详解 Vue 组件的完整生命周期
Vue 的生命周期是指一个组件从创建、挂载、更新到销毁的整个过程。在这个过程中,Vue 会自动执行一些钩子函数,让我们可以在特定阶段编写自己的逻辑。理解生命周期是掌握 Vue 的基础。
一、Vue 2 vs Vue 3 生命周期对比
Vue 3 对生命周期做了一些调整,但核心概念不变。以下是两者的对比:
Vue3
beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
beforeUnmount
unmounted
Vue2
beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
beforeDestroy
destroyed
| Vue 2 | Vue 3 | 说明 |
|---|---|---|
| beforeCreate | beforeCreate | 组件实例刚创建 |
| created | created | 数据观测完成 |
| beforeMount | beforeMount | 模板编译完成,还未挂载 |
| mounted | mounted | 挂载到 DOM |
| beforeUpdate | beforeUpdate | 数据更新前 |
| updated | updated | 数据更新完成 |
| beforeDestroy | beforeUnmount | 组件销毁前 |
| destroyed | unmounted | 组件已销毁 |
Vue 3 主要的变化是将 destroy 改为 unmount,语义更清晰。
二、各生命周期详解
2.1 beforeCreate
vue
<script>
export default {
beforeCreate() {
console.log('beforeCreate - 实例创建前');
// 此时 data、methods 还未初始化
console.log(this.$data); // undefined
}
}
</script>
执行时机:组件实例刚创建,还没有任何响应式数据。
适用场景:
- 初始化非响应式的数据
- 性能监控埋点
注意:这个阶段几乎不用,因为什么都还没准备好。
2.2 created
vue
<script>
export default {
data() {
return { message: 'Hello' }
},
created() {
console.log('created - 实例创建完成');
// data 已经可用
console.log(this.message); // 'Hello'
// 可以在此时发起接口请求
this.fetchData();
},
methods: {
async fetchData() {
const res = await fetch('/api/data');
this.data = await res.json();
}
}
}
</script>
执行时机:实例已完成数据观测(data)、方法(methods)的绑定。
适用场景:
- 发起初始接口请求
- 初始化组件状态
- 进行数据预处理
这是最常用的生命周期之一,常用于组件的初始化。
2.3 beforeMount
vue
<script>
export default {
beforeMount() {
console.log('beforeMount - 挂载前');
// 模板已经编译完成,但还未渲染到 DOM
console.log(this.$el); // undefined
// 可以访问 this.$refs(但此时还是空对象)
console.log(this.$refs.myDiv); // undefined
}
}
</script>
<template>
<div ref="myDiv">Hello</div>
</template>
执行时机:模板已经编译完成,生成 render 函数,但还未创建 DOM 节点。
适用场景:
- 几乎不用
- 某些需要提前获取 DOM 信息的场景(但不推荐)
2.4 mounted
vue
<script>
export default {
mounted() {
console.log('mounted - 挂载完成');
// DOM 已经渲染完成
console.log(this.$refs.myDiv); // <div>Hello</div>
// 第三方库初始化
this.initChart();
this.initEventListeners();
},
methods: {
initChart() {
// ECharts 初始化需要 DOM
this.chart = echarts.init(this.$refs.chart);
}
}
}
</script>
<template>
<div ref="chart" style="width: 100%; height: 400px;"></div>
</template>
执行时机:组件已经挂载到 DOM,DOM 节点已创建并可访问。
适用场景:
- 第三方库初始化(ECharts、Swiper 等)
- DOM 操作
- 绑定事件监听器
- 发起需要 DOM 的初始化请求
这是最常用的生命周期,用于需要操作 DOM 的场景。
2.5 beforeUpdate
vue
<script>
export default {
data() {
return { count: 0 }
},
beforeUpdate() {
console.log('beforeUpdate - 更新前');
// 可以在更新前获取更新前的 DOM 状态
console.log(this.$refs.counter.textContent); // 旧值
// 谨慎使用:这里修改数据会导致死循环
},
methods: {
increment() {
this.count++;
}
}
}
</script>
<template>
<button ref="counter" @click="increment">{{ count }}</button>
</template>
执行时机:数据发生变化,但 DOM 还未更新。
适用场景:
- 获取更新前的 DOM 状态
- 在 DOM 更新前移除事件监听
- 配合 updated 做性能优化
2.6 updated
vue
<script>
export default {
data() {
return { count: 0 }
},
updated() {
console.log('updated - 更新完成');
// DOM 已经更新完成
console.log(this.$refs.counter.textContent); // 新值
// 重新计算基于 DOM 的数据
this.recalculatePosition();
},
methods: {
recalculatePosition() {
// 基于新的 DOM 状态做处理
}
}
}
</script>
执行时机:DOM 已更新完成。
适用场景:
- 基于最新 DOM 做处理
- 重新计算布局
- 触发基于更新的动画
注意:避免在 updated 中修改 data,否则会触发死循环。
2.7 beforeUnmount(Vue 3)/ beforeDestroy(Vue 2)
vue
<script>
export default {
beforeUnmount() {
console.log('beforeUnmount - 销毁前');
// 清理定时器
clearInterval(this.timer);
clearTimeout(this.timeout);
// 移除事件监听
window.removeEventListener('resize', this.handleResize);
// 取消未完成的请求
this.controller?.abort();
},
mounted() {
this.timer = setInterval(() => {
this.fetchData();
}, 5000);
window.addEventListener('resize', this.handleResize);
}
}
</script>
执行时机:组件即将被销毁,但 DOM 还存在。
适用场景:
- 清理定时器
- 移除事件监听
- 取消未完成的网络请求
- 清理 Vuex 或 Pinia 订阅
2.8 unmounted(Vue 3)/ destroyed(Vue 2)
vue
<script>
export default {
unmounted() {
console.log('unmounted - 销毁完成');
// 理论上这里不应该再有 DOM 操作
// 但某些情况下可能还能访问到
// 可以做最终的清理工作
this.cleanup();
}
}
</script>
执行时机:组件已销毁,DOM 已移除。
适用场景:
- 几乎不用
- 调试用
三、Composition API 中的生命周期
Vue 3 的 Composition API 使用 setup 函数,生命周期钩子需要通过 import 引入:
vue
<script setup>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
// 等同于 beforeCreate + created
const message = ref('Hello')
onBeforeMount(() => {
console.log('onBeforeMount')
})
onMounted(() => {
console.log('onMounted')
// 第三方库初始化
echarts.init(document.querySelector('.chart'))
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate')
})
onUpdated(() => {
console.log('onUpdated')
})
onBeforeUnmount(() => {
// 清理定时器
clearInterval(timer)
})
onUnmounted(() => {
console.log('onUnmounted')
})
</script>
| 选项 API | Composition API |
|---|---|
| beforeCreate | setup(直接写) |
| created | setup(直接写) |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
四、生命周期实际应用
4.1 接口请求
vue
<script>
export default {
data() {
return {
list: [],
loading: false
}
},
// 推荐:created 中发起请求
created() {
this.fetchList();
},
// 或者:mounted 中发起请求(需要 DOM 时用)
mounted() {
this.initChart();
},
methods: {
async fetchList() {
this.loading = true;
try {
const res = await fetch('/api/list');
this.list = await res.json();
} finally {
this.loading = false;
}
}
}
}
</script>
4.2 第三方库初始化
vue
<script>
import echarts from 'echarts'
export default {
mounted() {
// ECharts 需要 DOM
this.chart = echarts.init(this.$refs.chart)
this.chart.setOption({
title: { text: 'ECharts' },
series: [{ type: 'pie', data: [1, 2, 3] }]
})
// 监听窗口变化
window.addEventListener('resize', this.handleResize)
},
beforeUnmount() {
// 清理
window.removeEventListener('resize', this.handleResize)
this.chart?.dispose()
},
methods: {
handleResize() {
this.chart?.resize()
}
}
}
</script>
<template>
<div ref="chart" style="width: 100%; height: 300px;"></div>
</template>
4.3 定时器管理
vue
<script>
export default {
data() {
return {
count: 0,
timer: null
}
},
mounted() {
// 启动定时器
this.timer = setInterval(() => {
this.count++;
}, 1000);
},
beforeUnmount() {
// 清理定时器(必须!)
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
}
}
</script>
4.4 路由守卫配合
vue
<script>
export default {
// 每次进入组件都会执行
created() {
// 从缓存页面回来时,created 不会再次执行
// 使用 watch 监听 $route 可以解决这个问题
this.fetchData();
},
watch: {
$route(to, from) {
// 路由参数变化时重新获取数据
if (to.params.id !== from.params.id) {
this.fetchData();
}
}
},
methods: {
fetchData() {
console.log('fetch data...');
}
}
}
</script>
五、KeepAlive 缓存与生命周期
使用 <keep-alive> 缓存组件时,生命周期会有所变化:
vue
<!-- 父组件 -->
<template>
<keep-alive :include="['UserList']">
<router-view />
</keep-alive>
</template>
再次进入
离开缓存
首次进入
首次进入
onMounted
onActivated
onActivated(不触发 mounted)
vue
<script>
export default {
// 第一次进入
mounted() {
console.log('mounted - 首次挂载');
this.fetchData();
},
// 被缓存的组件激活时
activated() {
console.log('activated - 组件激活');
// 每次从缓存回来都会执行
this.refreshData();
},
// 被缓存的组件停用时
deactivated() {
console.log('deactivated - 组件停用');
// 组件被缓存时执行
}
}
</script>
| 钩子 | 触发时机 |
|---|---|
| mounted | 首次渲染时执行 |
| activated | 每次从缓存激活时执行 |
| deactivated | 组件被缓存时执行 |
适用场景:
- 列表页缓存,返回时保留滚动位置
- 表单页缓存,返回时不丢失填写数据
六、父子组件生命周期执行顺序
6.1 创建过程
子组件 父组件 子组件 父组件 beforeCreate created beforeMount beforeCreate created beforeMount mounted mounted
javascript
// 父组件
created() { console.log('父 created') }
mounted() { console.log('父 mounted') }
// 子组件
created() { console.log('子 created') }
mounted() { console.log('子 mounted') }
// 输出顺序:
// 父 created
// 子 created
// 子 mounted
// 父 mounted
6.2 更新过程
javascript
// 父组件
beforeUpdate() { console.log('父 beforeUpdate') }
updated() { console.log('父 updated') }
// 子组件
beforeUpdate() { console.log('子 beforeUpdate') }
updated() { console.log('子 updated') }
// 当父组件数据变化触发更新:
// 父 beforeUpdate
// 子 beforeUpdate
// 子 updated
// 父 updated
6.3 销毁过程
javascript
// 父组件
beforeUnmount() { console.log('父 beforeUnmount') }
unmounted() { console.log('父 unmounted') }
// 子组件
beforeUnmount() { console.log('子 beforeUnmount') }
unmounted() { console.log('子 unmounted') }
// 销毁父组件时:
// 父 beforeUnmount
// 子 beforeUnmount
// 子 unmounted
// 父 unmounted
七、常见问题
7.1 created 和 mounted 有什么区别?
| 阶段 | created | mounted |
|---|---|---|
| DOM | 不可访问 | 可访问 |
| 数据 | 已响应式 | 已响应式 |
| 适用 | 接口请求 | DOM 操作 |
简单理解:需要操作 DOM 用 mounted,只需要数据用 created。
7.2 为什么定时器要在 beforeUnmount 清理?
vue
<!-- 不清理的后果 -->
<template>
<button @click="show = !show">切换</button>
<Child v-if="show" />
</template>
<script>
export default {
components: { Child },
data() { return { show: true } }
}
</script>
javascript
// Child.vue
mounted() {
this.timer = setInterval(() => {
console.log('timer running');
}, 1000);
}
// 切换 v-if 时,组件被销毁
// 如果不清理定时器,定时器还在运行
// 会导致内存泄漏和意外行为
7.3 为什么 updated 中修改数据会死循环?
javascript
updated() {
// 错误示例
this.count++; // 这会再次触发 updated
// 死循环!
}
// 正确做法:使用 watch 监听变化
watch: {
count(newVal) {
// 处理变化
}
}
7.4 异步组件的生命周期
vue
<script>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
</script>
<template>
<AsyncComponent v-if="show" />
<button @click="show = true">加载</button>
</template>
异步组件的生命周期:
- 加载中:触发 loading 钩子
- 加载成功:触发 resolved 钩子
- 加载失败:触发 error 钩子
- 实际组件:正常生命周期
八、总结
Vue 生命周期是开发中非常重要的概念,核心要点:
- created:最适合发起初始请求的时机
- mounted:需要操作 DOM 时的首选
- beforeUnmount:清理定时器、事件监听、取消请求
- keep-alive:使用 activated/deactivated 管理缓存组件
理解生命周期,能够帮助我们:
- 在正确的时机做正确的事
- 避免内存泄漏
- 优化性能
- 解决奇怪的问题