生命周期与模板引用
深入理解 Vue3 组件从创建到销毁的完整生命周期流程,掌握模板引用与组件引用的新用法。
一、前言
生命周期是 Vue 组件的核心概念之一,它描述了组件从创建、挂载、更新到卸载的完整过程。Vue3 在保留 Vue2 生命周期思想的基础上,引入了 Composition API,使得生命周期钩子的使用方式发生了显著变化。
本文将系统讲解 Vue3 的生命周期钩子、与 Vue2 的对比、模板引用的新用法,以及组件挂载的完整流程。
二、Vue3 生命周期钩子概览
2.1 生命周期钩子列表
Vue3 提供了以下生命周期钩子,需要在setup函数中显式导入使用:
| 生命周期钩子 | 触发时机 | Vue2 对应钩子 |
|---|---|---|
onBeforeMount | 组件挂载到 DOM 之前 | beforeMount |
onMounted | 组件挂载到 DOM 之后 | mounted |
onBeforeUpdate | 组件更新之前 | beforeUpdate |
onUpdated | 组件更新之后 | updated |
onBeforeUnmount | 组件卸载之前 | beforeDestroy |
onUnmounted | 组件卸载之后 | destroyed |
onErrorCaptured | 捕获后代组件错误时 | errorCaptured |
onRenderTracked | 调试:追踪响应式依赖 | — |
onRenderTriggered | 调试:触发重新渲染时 | — |
onActivated | 被keep-alive缓存的组件激活时 | activated |
onDeactivated | 被keep-alive缓存的组件停用时 | deactivated |
onServerPrefetch | 组件在服务器端渲染前 | serverPrefetch |
2.2 基本使用示例
<script setup> import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue' const count = ref(0) // 挂载前:DOM 尚未创建,此时无法访问模板引用 onBeforeMount(() => { console.log('onBeforeMount: 组件即将挂载') }) // 挂载后:可以安全地访问 DOM 和模板引用 onMounted(() => { console.log('onMounted: 组件已挂载') }) // 更新前:可以获取更新前的 DOM 状态 onBeforeUpdate(() => { console.log('onBeforeUpdate: 组件即将更新') }) // 更新后:DOM 已更新完成 onUpdated(() => { console.log('onUpdated: 组件已更新') }) // 卸载前:组件仍完全可用 onBeforeUnmount(() => { console.log('onBeforeUnmount: 组件即将卸载') }) // 卸载后:清理副作用(定时器、事件监听等) onUnmounted(() => { console.log('onUnmounted: 组件已卸载') }) </script> <template> <div> <p>计数: {{ count }}</p> <button @click="count++">增加</button> </div> </template>三、生命周期完整流程
3.1 生命周期流程图
3.2 组件挂载流程详解
Vue3 的组件挂载流程可以分为以下几个阶段:
- 创建阶段:调用
setup()函数,初始化响应式数据、计算属性和方法 - 编译阶段:将模板编译为渲染函数(开发环境下由 Vue 编译器完成)
- 挂载前阶段:执行
onBeforeMount,此时模板已编译但尚未插入 DOM - 挂载阶段:执行渲染函数,创建虚拟 DOM 并 diff,生成真实 DOM 插入页面
- 挂载完成阶段:执行
onMounted,此时可以安全访问 DOM 元素
<script setup> import { ref, onBeforeMount, onMounted } from 'vue' const message = ref('Hello Vue3') const containerRef = ref(null) // 挂载前:template 已编译,但 DOM 还未生成 onBeforeMount(() => { // 此时 containerRef.value 为 null console.log('before mount:', containerRef.value) // null }) // 挂载后:DOM 已生成,可以安全操作 onMounted(() => { // containerRef.value 指向真实 DOM 元素 console.log('mounted:', containerRef.value) // <div>Hello Vue3</div> containerRef.value.style.color = 'blue' }) </script> <template> <div ref="containerRef">{{ message }}</div> </template>四、Vue2 vs Vue3 生命周期对比
4.1 选项式 API vs 组合式 API
| 特性 | Vue2 Options API | Vue3 Composition API |
|---|---|---|
| 定义方式 | 在options对象中声明 | 在setup()中导入调用 |
| 代码组织 | 按选项类型分散 | 按逻辑功能聚合 |
this指向 | 指向组件实例 | 无this,通过闭包访问 |
| 复用逻辑 | Mixins(命名冲突、来源不明) | Composables(清晰、灵活) |
| 类型推断 | 较差 | 优秀的 TypeScript 支持 |
4.2 代码对比示例
Vue2 Options API:
exportdefault{data(){return{count:0,timer:null}},mounted(){this.timer=setInterval(()=>{this.count++},1000)},beforeDestroy(){clearInterval(this.timer)}}Vue3 Composition API:
<script setup> import { ref, onMounted, onUnmounted } from 'vue' const count = ref(0) let timer = null onMounted(() => { timer = setInterval(() => { count.value++ }, 1000) }) onUnmounted(() => { clearInterval(timer) }) </script>五、模板引用
5.1 基本用法
模板引用用于直接访问 DOM 元素或组件实例。在 Vue3 中,配合ref函数使用:
<script setup> import { ref, onMounted } from 'vue' // 声明模板引用 const inputRef = ref(null) const divRef = ref(null) onMounted(() => { // 挂载后自动填充值并聚焦 inputRef.value.value = '自动填充' inputRef.value.focus() // 操作 DOM 样式 divRef.value.style.backgroundColor = '#f0f0f0' }) </script> <template> <div> <!-- 绑定模板引用 --> <input ref="inputRef" type="text" placeholder="请输入" /> <div ref="divRef">这是一个 div 元素</div> </div> </template>5.2 v-for 中的模板引用
Vue3 中,v-for中的模板引用需要绑定到函数:
<script setup> import { ref, onMounted } from 'vue' const itemRefs = ref([]) const list = ref(['苹果', '香蕉', '橙子']) onMounted(() => { // itemRefs.value 是一个 DOM 元素数组 itemRefs.value.forEach((el, index) => { console.log(`第 ${index} 项:`, el.textContent) }) }) </script> <template> <ul> <li v-for="(item, index) in list" :key="index" :ref="(el) => { if (el) itemRefs[index] = el }" > {{ item }} </li> </ul> </template>5.3 组件引用
通过模板引用可以访问子组件的属性和方法:
<!-- ChildComponent.vue --> <script setup> import { ref } from 'vue' const count = ref(0) const increment = () => { count.value++ } const reset = () => { count.value = 0 } // 显式暴露给父组件 defineExpose({ count, increment, reset }) </script> <template> <div>子组件计数: {{ count }}</div> </template><!-- ParentComponent.vue --> <script setup> import { ref, onMounted } from 'vue' import ChildComponent from './ChildComponent.vue' const childRef = ref(null) const handleAdd = () => { // 调用子组件暴露的方法 childRef.value.increment() } const handleReset = () => { childRef.value.reset() console.log('当前计数:', childRef.value.count) } </script> <template> <div> <ChildComponent ref="childRef" /> <button @click="handleAdd">子组件 +1</button> <button @click="handleReset">子组件重置</button> </div> </template>5.4 $parent 与 $children 的变化
Vue3 中不再推荐使用$parent和$children:
| 特性 | Vue2 | Vue3 |
|---|---|---|
$parent | 直接访问父组件实例 | 仍可用但不推荐,破坏封装 |
$children | 直接访问子组件数组 | 已移除 |
| 推荐方案 | — | props/emits或provide/inject |
<script setup> // Vue3 中应优先使用 props 和 emits 进行父子通信 import { defineProps, defineEmits } from 'vue' const props = defineProps({ message: String }) const emit = defineEmits(['update']) const handleClick = () => { emit('update', '新消息') } </script>六、setup 中使用生命周期的注意事项
6.1 执行顺序
在setup中,生命周期钩子的注册顺序就是执行顺序:
<script setup> import { onMounted } from 'vue' // 多个 onMounted 会按注册顺序执行 onMounted(() => { console.log('第一个 onMounted') }) onMounted(() => { console.log('第二个 onMounted') }) </script>6.2 异步 setup
setup可以是异步函数,但需要注意:
<script setup> import { onMounted } from 'vue' // 异步 setup const data = await fetchData() // 顶层 await // 生命周期钩子仍然有效 onMounted(() => { console.log('挂载完成,数据:', data) }) </script>6.3 条件注册
生命周期钩子可以在条件语句中注册:
<script setup> import { ref, onMounted } from 'vue' const needTimer = ref(true) if (needTimer.value) { onMounted(() => { // 只在 needTimer 为 true 时注册 console.log('定时器模式已启动') }) } </script>七、调试钩子:onRenderTracked 与 onRenderTriggered
这两个钩子仅用于调试,帮助追踪响应式依赖:
<script setup> import { ref, onRenderTracked, onRenderTriggered } from 'vue' const count = ref(0) const name = ref('Vue3') // 追踪响应式依赖的收集 onRenderTracked((event) => { console.log('追踪到依赖:', event) // event.target: 被追踪的响应式对象 // event.type: 追踪类型 (get / has / iterate) // event.key: 被访问的属性键 }) // 追踪重新渲染的触发 onRenderTriggered((event) => { console.log('触发重新渲染:', event) // event.target: 触发更新的响应式对象 // event.type: 触发类型 (set / add / delete / clear) // event.key: 被修改的属性键 }) </script> <template> <div> <p>{{ name }} 计数: {{ count }}</p> <button @click="count++">增加</button> <button @click="name = 'Vue3 Pro'">改名</button> </div> </template>八、常见问题
Q1: 为什么 onMounted 中访问 ref 有时为 null?
通常是因为组件尚未挂载完成,或者使用了v-if条件渲染。确保在onMounted中访问,或使用nextTick:
import{ref,onMounted,nextTick}from'vue'constelRef=ref(null)onMounted(async()=>{awaitnextTick()// 确保 DOM 已更新console.log(elRef.value)})Q2: Vue3 中 destroyed 钩子去哪了?
Vue3 将beforeDestroy重命名为onBeforeUnmount,destroyed重命名为onUnmounted,命名更准确反映实际行为(卸载而非销毁)。
Q3: 如何在组合式函数中注册生命周期钩子?
组合式函数中可以直接使用生命周期钩子,它们会与调用组件的生命周期同步:
// useAutoIncrement.jsimport{ref,onMounted,onUnmounted}from'vue'exportfunctionuseAutoIncrement(initial=0,interval=1000){constcount=ref(initial)lettimer=nullonMounted(()=>{timer=setInterval(()=>{count.value++},interval)})onUnmounted(()=>{clearInterval(timer)})return{count}}Q4: 父子组件生命周期执行顺序是什么?
挂载阶段:父onBeforeMount→ 子onBeforeMount→ 子onMounted→ 父onMounted
卸载阶段:父onBeforeUnmount→ 子onBeforeUnmount→ 子onUnmounted→ 父onUnmounted
九、总结
Vue3 的生命周期系统保留了 Vue2 的核心概念,同时通过 Composition API 提供了更灵活的使用方式:
- 钩子命名更语义化:
destroyed→unmounted,更准确表达组件状态 - 显式导入:所有钩子需要从
vue导入,Tree-shaking 更友好 - 逻辑聚合:相关逻辑可以放在一起,不再分散在不同选项中
- 模板引用更简洁:配合
ref函数使用,类型推断更友好 - 组件引用需显式暴露:通过
defineExpose控制暴露接口,封装性更好
十、思考题/练习
代码练习:编写一个自定义组合式函数
useCountdown,实现倒计时功能,在组件卸载时自动清理定时器。对比分析:将一段 Vue2 的 Options API 代码(包含 data、mounted、beforeDestroy)改写为 Vue3 的 Composition API 版本。
生命周期顺序:画出包含三层嵌套组件(祖父 → 父 → 子)的挂载和卸载生命周期执行顺序图。
实践挑战:实现一个
useIntersectionObserver组合式函数,使用模板引用监听元素是否进入视口。