本文是 Vue3 系列第八篇,将深入探讨 Vue3 组件的生命周期。理解生命周期就像是了解一个人的成长历程,从出生到成长,再到衰老和离去,每个阶段都有其特定的意义和任务。掌握组件的生命周期,能够让我们在合适的时机执行合适的操作,构建出更加健壮和高效的 Vue 应用。
一、生命周期的深刻理解
人的生命周期类比
要理解组件的生命周期,我们可以用一个生动的比喻:把组件想象成一个人,它也有自己的"人生阶段"。
出生阶段(创建)
就像婴儿刚来到这个世界,组件也开始它的生命。在这个阶段,组件刚刚被创建,但还没有真正出现在界面上。就像婴儿有了生命体征,但还没有开始与外界互动。
成长阶段(挂载)
这个阶段相当于人开始接触世界、学习知识。组件被添加到 DOM 中,开始与用户交互,显示内容,响应操作。就像孩子开始上学,认识朋友,参与社会活动。
变化阶段(更新)
人生中会经历很多变化,比如换工作、搬家、学习新技能。同样,当组件的数据发生变化时,它需要更新自己来反映这些变化。这个阶段组件会重新渲染,展示最新的状态。
离别阶段(卸载)
就像生命终有尽头,组件也会在不再需要时被销毁。在这个阶段,组件会清理自己占用的资源,移除事件监听器,准备从内存中消失。
组件的四个核心生命周期阶段
每个 Vue 组件都会经历四个主要的生命周期阶段:
-
创建阶段(Creation):组件实例被创建,但还没有挂载到 DOM
-
挂载阶段(Mounting):组件被挂载到 DOM,开始与用户交互
-
更新阶段(Updating):响应数据变化,重新渲染组件
-
卸载阶段(Unmounting):组件被销毁,清理资源
特定时刻调用特定函数的意义
生命周期的核心价值在于:在特定的时刻执行特定的代码。这就像我们在人生的重要节点做出重要决定一样:
-
出生时:准备必要的生存条件
-
上学时:获取知识和技能
-
工作时:发挥能力创造价值
-
退休时:整理一生,安享晚年
在组件开发中,我们利用生命周期函数来:
-
在组件创建时初始化数据
-
在挂载完成后操作 DOM
-
在更新前后执行特定逻辑
-
在销毁前清理资源,避免内存泄漏
二、Vue2 的生命周期详解
创建阶段:beforeCreate() 和 created()
让我们通过一个简单的计数器组件来理解 Vue2 的生命周期:
html
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
现在我们在其中添加创建阶段的钩子函数:
html
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
// 创建阶段开始 - 组件实例刚被创建
beforeCreate() {
console.log('beforeCreate - 组件实例刚创建')
console.log('数据 count:', this.count) // undefined
console.log('方法 increment:', this.increment) // undefined
},
// 创建阶段完成 - 数据观测和事件配置已完成
created() {
console.log('created - 数据观测已完成')
console.log('数据 count:', this.count) // 0
console.log('方法 increment:', this.increment) // 函数
},
methods: {
increment() {
this.count++
}
}
}
</script>
创建阶段的详细解说:
beforeCreate()
-
时机:在组件实例刚被创建时调用
-
状态:数据观测(data)和事件配置(methods)都尚未初始化
-
用途:通常用于插件开发,普通业务中很少使用
created()
-
时机:在组件实例创建完成后调用
-
状态:数据观测已完成,方法和计算属性可用,但 DOM 还未生成
-
用途:常用于数据初始化、API 调用等不需要操作 DOM 的任务
这个阶段就像是婴儿刚刚出生,有了基本的生命体征(数据和方法),但还没有开始与外界互动(DOM 操作)。
挂载阶段:beforeMount() 和 mounted()
现在让我们看看挂载阶段的钩子函数:
html
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
// 挂载阶段开始 - 模板编译完成,即将挂载到 DOM
beforeMount() {
console.log('beforeMount - 模板编译完成,即将挂载')
console.log('DOM 元素:', this.$el) // undefined,因为还没挂载
},
// 挂载阶段完成 - 组件已挂载到 DOM
mounted() {
console.log('mounted - 组件已挂载到 DOM')
console.log('DOM 元素:', this.$el) // 实际的 DOM 元素
console.log('计数元素:', this.$el.querySelector('p').textContent)
},
methods: {
increment() {
this.count++
}
}
}
</script>
挂载阶段的详细解说:
beforeMount()
-
时机:在挂载开始之前调用,模板已经编译完成
-
状态:虚拟 DOM 已创建,但尚未转换为真实 DOM
-
用途:在组件首次渲染前执行最后准备
mounted()
-
时机:在组件挂载到 DOM 后调用
-
状态:组件已完全初始化,可以访问 DOM 元素
-
用途:常用于需要操作 DOM 的任务,如初始化第三方库、添加事件监听器等
这个阶段就像是人开始步入社会,具备了与外界交互的能力,可以开始施展才华了。
更新阶段:beforeUpdate() 和 updated()
当组件数据发生变化时,会进入更新阶段:
html
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
// 更新阶段开始 - 数据变化,虚拟 DOM 重新渲染之前
beforeUpdate() {
console.log('beforeUpdate - 数据已变化,即将重新渲染')
console.log('当前计数:', this.count)
console.log('DOM 内容:', this.$el.querySelector('p').textContent)
},
// 更新阶段完成 - 虚拟 DOM 已重新渲染并更新到真实 DOM
updated() {
console.log('updated - 组件已更新')
console.log('当前计数:', this.count)
console.log('DOM 内容:', this.$el.querySelector('p').textContent)
},
methods: {
increment() {
this.count++
}
}
}
</script>
更新阶段的详细解说:
beforeUpdate()
-
时机:在数据变化后,虚拟 DOM 重新渲染之前调用
-
状态:数据已更新,但界面还未反映变化
-
用途:在更新前获取当前状态,或执行更新前的逻辑
updated()
-
时机:在数据变化导致虚拟 DOM 重新渲染和更新后调用
-
状态:组件已更新,DOM 已同步到最新状态
-
用途:执行依赖于更新后 DOM 的操作
这个阶段就像是人在生活中不断学习和成长,适应新的环境和挑战。
销毁阶段:beforeDestroy() 和 destroyed()
当组件不再需要时,会进入销毁阶段:
html
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
<button @click="$destroy()">销毁组件</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0,
timer: null
}
},
mounted() {
this.timer = setInterval(() => {
console.log('计时器运行中...')
}, 1000)
},
// 销毁阶段开始 - 组件实例销毁之前
beforeDestroy() {
console.log('beforeDestroy - 组件即将销毁')
// 清理工作,如清除定时器、取消事件监听等
clearInterval(this.timer)
console.log('计时器已清理')
},
// 销毁阶段完成 - 组件实例已销毁
destroyed() {
console.log('destroyed - 组件已销毁')
console.log('数据 count:', this.count) // 仍可访问,但已无意义
},
methods: {
increment() {
this.count++
}
}
}
</script>
销毁阶段的详细解说:
beforeDestroy()
-
时机:在组件实例销毁之前调用
-
状态:组件实例仍然完全可用
-
用途:执行清理操作,如清除定时器、取消网络请求、移除事件监听等
destroyed()
-
时机:在组件实例销毁之后调用
-
状态:组件实例的所有指令都已解绑,事件监听器已移除
-
用途:执行最终的清理工作
这个阶段就像是人生的晚年,整理一生的经历,安排好身后事,平静地离开。
三、Vue3 的生命周期详解
Vue3 在保留 Vue2 生命周期概念的基础上,进行了一些重要的改进和优化。最大的变化是引入了组合式 API,让生命周期函数的使用更加灵活。
创建阶段的改变
在 Vue3 中,创建阶段的行为有所变化:
html
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
// 在 Vue3 的 <script setup> 中,代码会立即执行
console.log('这相当于在 created 阶段执行')
console.log('数据 count:', count.value)
console.log('方法 increment:', increment)
</script>
Vue3 创建阶段的变化:
-
没有 beforeCreate 和 created:在组合式 API 中,这两个钩子被 setup 函数替代
-
setup 函数执行时机:相当于在 beforeCreate 和 created 之间执行
-
更直观的代码组织 :在
<script setup>中的代码会在组件创建时立即执行
这种改变让代码更加直观,我们不需要再记忆特定的钩子函数名称,只需要在 setup 中编写初始化逻辑即可。
挂载阶段:onBeforeMount() 和 onMounted()
Vue3 的挂载阶段钩子以函数形式提供:
html
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script setup>
import { ref, onBeforeMount, onMounted } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
// 挂载前钩子
onBeforeMount(() => {
console.log('onBeforeMount - 即将挂载到 DOM')
console.log('DOM 元素还未可用')
})
// 挂载后钩子
onMounted(() => {
console.log('onMounted - 已挂载到 DOM')
console.log('现在可以操作 DOM 元素')
const countElement = document.querySelector('p')
console.log('计数元素:', countElement.textContent)
})
</script>
Vue3 挂载阶段的特点:
-
函数式 API:生命周期钩子以函数形式导入和使用
-
更好的 TypeScript 支持:完整的类型推断
-
更灵活的代码组织:可以在任何地方使用生命周期钩子
-
相同的执行时机:与 Vue2 的对应钩子执行时机相同
更新阶段:onBeforeUpdate() 和 onUpdated()
Vue3 的更新阶段钩子:
html
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script setup>
import { ref, onBeforeUpdate, onUpdated } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
// 更新前钩子
onBeforeUpdate(() => {
console.log('onBeforeUpdate - 数据已变化,即将重新渲染')
console.log('当前计数:', count.value)
})
// 更新后钩子
onUpdated(() => {
console.log('onUpdated - 组件已更新')
console.log('当前计数:', count.value)
})
</script>
卸载阶段:onBeforeUnmount() 和 onUnmounted()
Vue3 的卸载阶段钩子名称有所变化:
html
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script setup>
import { ref, onBeforeUnmount, onUnmounted } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
// 卸载前钩子
onBeforeUnmount(() => {
console.log('onBeforeUnmount - 组件即将卸载')
// 清理工作
})
// 卸载后钩子
onUnmounted(() => {
console.log('onUnmounted - 组件已卸载')
})
</script>
Vue3 卸载阶段的变化:
-
名称变更 :
beforeDestroy→onBeforeUnmount,destroyed→onUnmounted -
更准确的语义:"unmount" 比 "destroy" 更准确地描述了组件从 DOM 中移除的过程
-
相同的功能:执行时机和用途与 Vue2 对应钩子相同
父子组件生命周期执行顺序
这是一个非常重要且容易混淆的概念。让我们通过示例来理解:
父组件:
html
<template>
<div>
<h3>父组件</h3>
<ChildComponent v-if="showChild" />
<button @click="showChild = !showChild">切换子组件</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
const showChild = ref(true)
onMounted(() => {
console.log('父组件 onMounted')
})
</script>
子组件:
html
<template>
<div>
<h4>子组件</h4>
<p>我是子组件</p>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log('子组件 onMounted')
})
</script>
执行结果分析:
当你运行这个示例时,控制台的输出顺序是:
html
子组件 onMounted
父组件 onMounted
深度优先的挂载顺序:
Vue 采用深度优先的组件树遍历策略:
-
父组件开始挂载
-
遇到子组件,暂停父组件的挂载,先挂载子组件
-
子组件完全挂载完成后,继续完成父组件的挂载
-
所有子组件都挂载完成后,父组件才完成挂载
这就像建造一栋大楼:
-
先打好地基(父组件开始)
-
然后建造每一层楼的房间(子组件)
-
所有房间都建好后,大楼才算建成(父组件完成)
这种深度优先的策略确保了子组件在父组件之前完全初始化,避免了父组件依赖未准备好的子组件的情况。
四、常用的生命周期钩子
在实际开发中,有些生命周期钩子使用频率很高,而有些则相对较少。
最常用的钩子
onMounted - 使用频率最高
TypeScript
onMounted(() => {
// DOM 操作
// 第三方库初始化
// API 数据获取
// 事件监听器添加
})
onUnmounted - 资源清理
TypeScript
onUnmounted(() => {
// 清除定时器
// 取消网络请求
// 移除事件监听器
// 清理第三方库实例
})
onUpdated - 响应数据变化
TypeScript
onUpdated(() => {
// 依赖于更新后 DOM 的操作
// 跟踪特定的数据变化
})
五、总结
通过本文的深入学习,相信你已经对 Vue3 的生命周期有了全面而深刻的理解。
核心要点回顾
生命周期是组件从创建到销毁的完整过程,Vue 在关键节点提供了钩子函数,让我们能够在特定时刻执行特定代码。理解生命周期有助于我们编写更加健壮和高效的组件。
Vue2 与 Vue3 的生命周期对比
相同点:
-
生命周期阶段划分相同:创建、挂载、更新、卸载
-
各阶段的执行时机和用途基本相同
-
核心概念保持一致
不同点:
-
API 形式:Vue2 是选项式,Vue3 是函数式
-
创建阶段:Vue3 用 setup 替代 beforeCreate/created
-
命名调整:销毁相关钩子更名为卸载相关
-
代码组织:Vue3 提供更灵活的代码组织方式
生命周期执行顺序的关键理解
-
深度优先:子组件总是在父组件之前完成挂载
-
同步执行:生命周期钩子按照特定顺序同步执行
-
可预测性:理解执行顺序有助于调试和优化
下一节我们将一起探讨Hooks。
关于 Vue3 生命周期有任何疑问?欢迎在评论区提出,我们会详细解答!