第105课:生命周期与模板引用——钩子函数、ref 模板引用、nextTick
每个 Vue 组件从创建到销毁都会经历一系列步骤:初始化响应式数据、编译模板、挂载到 DOM、响应数据变化更新 DOM、最终卸载。Vue 在这些关键节点暴露了生命周期钩子函数,允许开发者在特定阶段执行自定义逻辑。在组合式 API 中,这些钩子以 onMounted、onUpdated、onUnmounted 等函数的形式存在。同时,Vue 提供了模板引用(Template Refs) 机制,通过 ref 属性直接获取 DOM 元素或子组件实例的引用,配合 nextTick 在 DOM 更新完成后执行操作。理解生命周期的执行顺序和模板引用的可用时机,是处理 DOM 交互、第三方库集成和资源管理的基础。
1. 组合式 API 的生命周期钩子
在选项式 API 中,生命周期以组件选项的形式存在(如 mounted、updated)。在组合式 API 中,它们是对应的函数,需要在 setup 或 <script setup> 中调用。每个钩子函数接收一个回调,该回调在对应的生命周期阶段执行。
1.1 完整生命周期图示与钩子映射
| 选项式 API | 组合式 API (setup 内) |
触发时机 |
|---|---|---|
beforeCreate |
直接写在 setup 顶部 |
组件实例初始化前(setup 本身就在此阶段执行)。 |
created |
直接写在 setup 顶部 |
组件实例创建完成,响应式数据已就绪。 |
beforeMount |
onBeforeMount |
组件即将挂载到 DOM 之前。 |
mounted |
onMounted |
组件挂载到 DOM 之后,可以访问模板引用的 DOM 元素。 |
beforeUpdate |
onBeforeUpdate |
响应式数据变化导致组件重新渲染之前。 |
updated |
onUpdated |
组件重新渲染并更新 DOM 之后。 |
beforeUnmount |
onBeforeUnmount |
组件即将卸载之前(DOM 仍存在)。 |
unmounted |
onUnmounted |
组件卸载之后(DOM 已移除,事件监听和定时器应在此清理)。 |
errorCaptured |
onErrorCaptured |
捕获后代组件传递的错误。 |
activated |
onActivated |
<KeepAlive> 缓存的组件激活时。 |
deactivated |
onDeactivated |
<KeepAlive> 缓存的组件停用时。 |
serverPrefetch |
onServerPrefetch |
服务端渲染时预取数据(SSR 专属)。 |
1.2 setup 替代了 beforeCreate 和 created
在组合式 API 中,setup() 函数本身就是在 beforeCreate 和 created 之间执行的。因此在这两个阶段需要执行的逻辑(如初始化响应式数据、设置侦听器)直接写在 setup 顶层即可,无需额外钩子。
1 | <script setup> |
1.3 父子组件生命周期执行顺序
当父组件和子组件都有生命周期钩子时,执行顺序为:
- 挂载阶段:父
beforeMount→ 子beforeMount→ 子mounted→ 父mounted - 更新阶段:父
beforeUpdate→ 子beforeUpdate→ 子updated→ 父updated - 卸载阶段:父
beforeUnmount→ 子beforeUnmount→ 子unmounted→ 父unmounted
记忆技巧:父组件先“准备”(before),子组件先“完成”(mounted/updated/unmounted)。Vue 的渲染是自外向内、挂载是自内向外。
1.4 常用钩子场景
| 钩子 | 典型用途 |
|---|---|
onMounted |
发起初始网络请求、初始化第三方库(Chart.js、Swiper)、操作模板引用的 DOM。 |
onBeforeUpdate |
在 DOM 更新前手动读取即将被更新的 DOM 状态(如滚动位置),在 onUpdated 中恢复。 |
onUpdated |
DOM 更新后执行操作(但注意避免在此修改响应式数据,否则可能无限循环)。 |
onBeforeUnmount |
在组件卸载前执行清理(移除事件监听、销毁第三方实例)。与 onUnmounted 相比,此时 DOM 仍存在。 |
onUnmounted |
清理定时器、取消订阅、释放资源。与 watch 和 watchEffect 的自动清理不同,手动创建的副作用必须在此手动停止。 |
2. 模板引用:ref 属性与响应式引用
在 Vue 的声明式渲染模型中,你通常不需要直接操作 DOM。但有时必须触及底层 DOM 元素:聚焦输入框、测量元素尺寸、与第三方非 Vue 库集成。Vue 通过模板引用(Template Refs)提供了安全的 DOM 访问方式。
2.1 获取单个 DOM 元素
在 <script setup> 中,声明一个与模板中 ref 属性同名的 ref 变量(初始值为 null)。Vue 会在组件挂载后将对应的 DOM 元素赋值给 ref.value,在组件卸载前重新设为 null。
1 | <script setup> |
访问时机:ref.value 在首次渲染时为 null(因为 DOM 尚未生成)。它仅在 onMounted 钩子及之后的生命周期中才指向有效的 DOM 节点。不能在 setup 顶层或模板表达式中(如 {{ inputRef.value }})直接使用它来驱动 UI——因为 ref 的变化不会触发重新渲染(模板引用 ref 与响应式 ref 不同,它被赋值为 DOM 元素后不会触发响应式更新)。
2.2 v-for 中的模板引用:获取元素数组
当 ref 属性用在 v-for 循环中时,对应的 ref 变量会被赋值为一个包含所有匹配 DOM 元素的数组(顺序与源数据一致)。
1 | <script setup> |
注意事项:
itemRefs.value数组的顺序与源数组一致,而非 DOM 中的物理顺序。- 当
items数组更新时(添加/删除元素),itemRefs.value会自动更新(Vue 内部维护)。 - 在
onUpdated中访问itemRefs.value可获取更新后的元素数组。
2.3 函数型模板引用
除了传递 ref 对象,ref 属性还可以绑定一个函数。该函数在元素被挂载时调用(参数为 DOM 元素),在元素被卸载时再次调用(参数为 null)。函数型 ref 比对象型 ref 更灵活,尤其适合需要在 ref 挂载/卸载时执行逻辑的场景。
1 | <script setup> |
函数型 ref 的优势:你不需要预先声明一个 ref 变量,且可以在挂载/卸载时立即执行自定义逻辑(如初始化或销毁第三方库实例),而无需在 onMounted 和 onUnmounted 中手动管理。
2.4 获取子组件实例:defineExpose
模板引用不仅可以获取 DOM 元素,还可以获取子组件实例。但在 <script setup> 中,子组件默认是封闭的——父组件无法直接访问子组件内部的任何属性或方法。子组件必须通过 defineExpose 显式暴露接口。
子组件 (ChildModal.vue):
1 | <script setup> |
父组件:
1 | <script setup> |
defineExpose 的设计哲学:在选项式 API 中,父组件通过 this.$refs.xxx 可以访问子组件的所有属性和方法,这破坏了封装性。组合式 API 通过 defineExpose 强制开发者明确声明哪些接口是对外公开的,这是“封装”原则在组件 API 中的体现。父组件只能访问子组件显式暴露的部分,无权触及内部实现细节。
3. nextTick:等待 DOM 更新完成
3.1 问题:响应式数据变化后,DOM 不会立即更新
Vue 的响应式更新是异步批处理的。当你修改一个 ref 的值时,Vue 不会立即更新 DOM,而是将更新推入队列,在当前同步代码执行完毕后统一刷新。这种机制避免了多次数据变更触发多次不必要的 DOM 更新,显著提升了性能。但这也意味着:如果你在修改数据后立即读取 DOM,读到的仍然是旧状态。
1 | <script setup> |
3.2 nextTick 的两种调用方式
1 | // 回调方式 |
3.3 典型场景
| 场景 | 使用方式 |
|---|---|
| 修改数据后立即聚焦输入框 | await nextTick(); inputRef.value.focus(); |
| 动态添加列表项后滚动到底部 | await nextTick(); listRef.value.scrollTop = ... |
| 切换显示状态后初始化第三方库(如图表) | await nextTick(); new Chart(canvasRef.value); |
| 多个数据变更完成后统一读取一次 DOM | 在所有修改后只调用一次 await nextTick() |
4. 生命周期与模板引用的协作模式
4.1 在 onMounted 中安全访问模板引用
onMounted 是模板引用 ref.value 从 null 变为有效 DOM 元素的最早时机。任何依赖 DOM 存在的初始化逻辑(如初始化滚动条、Canvas、第三方库)都应在此钩子中进行。
1 | <script setup> |
4.2 在 onBeforeUnmount 中清理副作用
手动添加的 DOM 事件监听、定时器、第三方库实例必须在组件卸载前清理,否则会造成内存泄漏。
1 | <script setup> |
4.3 在 onUpdated 中响应 DOM 变化
当响应式数据变化导致 DOM 更新后,onUpdated 钩子触发。你可以在此钩子中根据新的 DOM 状态执行操作。但需要注意避免修改响应式数据,否则可能触发无限更新循环。
1 | <script setup> |
4.4 watch 配合 nextTick:更精确的 DOM 响应
如果你需要在特定数据变化后(而非任何数据变化后)操作 DOM,使用 watch 配合 nextTick 比 onUpdated 更精确。
1 | const showEditor = ref(false); |
5. 综合实战:带聚焦和滚动功能的聊天组件
1 | <script setup> |
设计要点:
onMounted中聚焦输入框,确保用户可以直接输入。- 发送消息后使用
await nextTick()等待 DOM 更新,然后滚动到底部。 - 键盘事件使用
@keydown而非@keyup,更快速地响应回车键。 onBeforeUnmount中输出日志(实际项目可能清理定时器或事件监听)。- 模板引用
listRef和inputRef分别操作滚动容器和输入框。
课后练习
一、概念自测(选择题 / 填空题)
(单选) 在 Vue 3 组合式 API 中,最早能安全访问模板引用(
ref.value指向有效 DOM 元素)的钩子是?
A.onBeforeMount
B.onMounted
C.setup顶层
D.onBeforeCreate(单选) 关于
nextTick的描述,正确的是?
A. 它会立即重新渲染整个组件。
B. 它返回一个 Promise,在 DOM 更新队列清空后解析。
C. 它是同步执行的。
D. 它仅适用于修改ref后的操作,对reactive无效。(填空) 在
<script setup>中,要让父组件通过模板引用访问子组件的方法,子组件必须使用______宏显式暴露这些方法。(多选) 以下哪些操作适合在
onMounted钩子中执行?
A. 发起初始网络请求。
B. 初始化第三方图表库。
C. 清理定时器。
D. 通过模板引用聚焦输入框。
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要实现一个可展开的 FAQ 手风琴组件。要求如下:
- 每个 FAQ 项包含问题和答案,默认只显示问题,答案折叠。
- 点击问题标题时,展开该答案并折叠其他已展开的答案(手风琴模式)。
- 使用
nextTick在答案展开后,自动滚动该答案区域到视口中央。 - 使用模板引用获取每个答案区域的 DOM 元素(通过
v-for中的ref数组)。 - 在
onMounted中自动展开第一个 FAQ 项。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 Vue 3 单文件组件。提示词中需明确指定 nextTick 的使用时机、v-for 中模板引用的写法、以及 onMounted 中自动展开的逻辑。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请创建一个 Vue 3 FAQ 手风琴组件。需要创建文件
src/components/FaqAccordion.vue:
- 使用
<script setup>。- 定义
faqItems数组,包含 4-5 个{ id, question, answer }对象。- 定义
expandedIndexref,初始null;answerRefsref,初始[]。toggle(index)方法:若当前expandedIndex.value === index,则设为null(折叠);否则设为index(展开)。- 在
toggle中,若展开某个项,使用await nextTick()后调用answerRefs.value[index]?.scrollIntoView({ behavior: 'smooth', block: 'center' })。onMounted中自动调用toggle(0)展开第一项。- 模板使用
v-for渲染 FAQ 项。每个项包含一个点击触发的标题区域和一个条件渲染的答案区域(v-if)。答案区域的ref使用:ref="el => { if (el) answerRefs[index] = el }"函数型 ref(或直接使用ref="answerRefs"数组型)。- 答案区域样式需有过渡效果(可使用
<Transition>包裹)。- 所有代码添加 JSDoc 注释。完成后输出完整文件内容。
四、面试真题与参考答案
题目(滴滴前端面试题):
请解释 Vue 3 中
nextTick的原理和使用场景。为什么 Vue 采用异步更新队列?在父子组件的挂载过程中,onMounted的执行顺序是怎样的?如果你在父组件的setup中修改了一个 ref 值,然后立即在子组件的模板中引用该值,子组件会立即渲染出新值吗?为什么?
参考答案:
nextTick 的原理:Vue 内部维护了一个异步更新队列。当响应式数据变化时,Vue 不会立即更新 DOM,而是将组件的渲染函数推入该队列,并在当前同步代码执行完毕后通过微任务(Promise)或宏任务批量执行队列。nextTick 返回一个 Promise,解析时机就在队列清空、DOM 更新完成之后。
Vue 采用异步更新队列的原因:如果每次数据变化都立即更新 DOM,会导致大量不必要的渲染和布局计算。例如,一个循环中修改 100 次 count,同步模式将触发 100 次 DOM 更新;而异步批处理仅触发 1 次。这极大提升了性能。
父子组件的 onMounted 执行顺序:子组件先于父组件。因为挂载过程是:父组件 setup → 父 beforeMount → 子 setup → 子 beforeMount → 子 mounted → 父 mounted。父组件必须等待其所有子组件完成挂载后,自身才算真正挂载完成。
在父组件 setup 中修改 ref 值后,子组件模板并不会立即渲染出新值,因为整个渲染更新仍然是异步的。父组件的 setup 在组件实例创建阶段执行,此时模板尚未开始渲染。子组件模板的渲染发生在父组件模板渲染的过程中(如果子组件在父模板中被引用),而整个过程被推入异步更新队列,最终在微任务中统一执行。因此,在 setup 中修改 ref 后,当前同步代码中无法获取更新后的 DOM,必须通过 nextTick 等待。
课后练习答案
一、概念自测答案
B
- 解析:
onMounted是组件挂载到 DOM 后触发的钩子,此时模板引用的 DOM 元素已就绪。onBeforeMount阶段 DOM 尚未生成。
- 解析:
B
- 解析:
nextTick返回一个 Promise,在 DOM 更新队列清空后解析。A 错误(不立即渲染),C 错误(异步的),D 错误(对reactive也有效)。
- 解析:
defineExpose- 解析:
<script setup>中的子组件通过defineExpose({ ... })暴露接口给父组件的模板引用。
- 解析:
A、B、D
- 解析:C 错误,清理定时器应在
onBeforeUnmount或onUnmounted中执行。A(网络请求)、B(初始化第三方库)、D(操作 DOM 元素)均适合onMounted。
- 解析:C 错误,清理定时器应在
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请用 Vue 3<script setup>实现一个手风琴 FAQ 组件。要求:
- FAQ 数据为静态数组,每项有 id、question、answer。
- 点击问题标题展开/折叠答案(手风琴模式,仅一个展开)。
- 使用
nextTick在答案展开后自动滚动到视口中央(scrollIntoView)。- 使用
v-for中的模板引用获取答案区域 DOM。onMounted自动展开第一个。- 输出完整单文件组件,包含必要样式。”