WinddSnow

Vue-3-Lifecycle-and-Template-Refs-onMounted-ref-nextTick

字数统计: 5.3k阅读时长: 22 min
2026/08/01

第105课:生命周期与模板引用——钩子函数、ref 模板引用、nextTick

每个 Vue 组件从创建到销毁都会经历一系列步骤:初始化响应式数据、编译模板、挂载到 DOM、响应数据变化更新 DOM、最终卸载。Vue 在这些关键节点暴露了生命周期钩子函数,允许开发者在特定阶段执行自定义逻辑。在组合式 API 中,这些钩子以 onMountedonUpdatedonUnmounted 等函数的形式存在。同时,Vue 提供了模板引用(Template Refs) 机制,通过 ref 属性直接获取 DOM 元素或子组件实例的引用,配合 nextTick 在 DOM 更新完成后执行操作。理解生命周期的执行顺序和模板引用的可用时机,是处理 DOM 交互、第三方库集成和资源管理的基础。


1. 组合式 API 的生命周期钩子

在选项式 API 中,生命周期以组件选项的形式存在(如 mountedupdated)。在组合式 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 替代了 beforeCreatecreated

在组合式 API 中,setup() 函数本身就是在 beforeCreatecreated 之间执行的。因此在这两个阶段需要执行的逻辑(如初始化响应式数据、设置侦听器)直接写在 setup 顶层即可,无需额外钩子。

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import { ref, onMounted } from 'vue';

// 这里就是 beforeCreate / created 阶段
const count = ref(0);
const message = ref('Hello');

// mounted 阶段
onMounted(() => {
console.log('DOM 已挂载,可以访问模板引用');
});
</script>

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 清理定时器、取消订阅、释放资源。watchwatchEffect 的自动清理不同,手动创建的副作用必须在此手动停止

2. 模板引用:ref 属性与响应式引用

在 Vue 的声明式渲染模型中,你通常不需要直接操作 DOM。但有时必须触及底层 DOM 元素:聚焦输入框、测量元素尺寸、与第三方非 Vue 库集成。Vue 通过模板引用(Template Refs)提供了安全的 DOM 访问方式。

2.1 获取单个 DOM 元素

<script setup> 中,声明一个与模板中 ref 属性同名的 ref 变量(初始值为 null)。Vue 会在组件挂载后将对应的 DOM 元素赋值给 ref.value,在组件卸载前重新设为 null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { ref, onMounted } from 'vue';

// 声明模板引用变量,名称必须与模板中的 ref 属性一致
const inputRef = ref(null);

onMounted(() => {
// 此时 inputRef.value 是真实的 <input> DOM 元素
inputRef.value.focus();
});
</script>

<template>
<input ref="inputRef" placeholder="输入内容" />
</template>

访问时机ref.value首次渲染时null(因为 DOM 尚未生成)。它仅在 onMounted 钩子及之后的生命周期中才指向有效的 DOM 节点。不能在 setup 顶层或模板表达式中(如 {{ inputRef.value }})直接使用它来驱动 UI——因为 ref 的变化不会触发重新渲染(模板引用 ref 与响应式 ref 不同,它被赋值为 DOM 元素后不会触发响应式更新)。

2.2 v-for 中的模板引用:获取元素数组

ref 属性用在 v-for 循环中时,对应的 ref 变量会被赋值为一个包含所有匹配 DOM 元素的数组(顺序与源数据一致)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup>
import { ref, onMounted } from 'vue';

const items = ref([{ id: 1, text: 'A' }, { id: 2, text: 'B' }, { id: 3, text: 'C' }]);
const itemRefs = ref([]);

onMounted(() => {
console.log(itemRefs.value.length); // 3
itemRefs.value.forEach((el, index) => {
console.log(`元素 ${index}:`, el.textContent);
});
});
</script>

<template>
<ul>
<li v-for="item in items" :key="item.id" ref="itemRefs">{{ item.text }}</li>
</ul>
</template>

注意事项

  • itemRefs.value 数组的顺序与源数组一致,而非 DOM 中的物理顺序。
  • items 数组更新时(添加/删除元素),itemRefs.value 会自动更新(Vue 内部维护)。
  • onUpdated 中访问 itemRefs.value 可获取更新后的元素数组。

2.3 函数型模板引用

除了传递 ref 对象,ref 属性还可以绑定一个函数。该函数在元素被挂载时调用(参数为 DOM 元素),在元素被卸载时再次调用(参数为 null)。函数型 ref 比对象型 ref 更灵活,尤其适合需要在 ref 挂载/卸载时执行逻辑的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
function onListItemMounted(el) {
if (el) {
console.log('列表项挂载:', el.textContent);
} else {
console.log('列表项卸载');
}
}
</script>

<template>
<ul>
<li v-for="item in items" :key="item.id" :ref="onListItemMounted">{{ item.text }}</li>
</ul>
</template>

函数型 ref 的优势:你不需要预先声明一个 ref 变量,且可以在挂载/卸载时立即执行自定义逻辑(如初始化或销毁第三方库实例),而无需在 onMountedonUnmounted 中手动管理。

2.4 获取子组件实例:defineExpose

模板引用不仅可以获取 DOM 元素,还可以获取子组件实例。但在 <script setup> 中,子组件默认是封闭的——父组件无法直接访问子组件内部的任何属性或方法。子组件必须通过 defineExpose 显式暴露接口。

子组件 (ChildModal.vue):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup>
import { ref } from 'vue';

const visible = ref(false);

function open() {
visible.value = true;
}

function close() {
visible.value = false;
}

// 显式暴露给父组件的方法和属性
defineExpose({ open, close, visible });
</script>

<template>
<div v-if="visible" class="modal">
<slot />
<button @click="close">关闭</button>
</div>
</template>

父组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup>
import { ref } from 'vue';
import ChildModal from './ChildModal.vue';

const modalRef = ref(null);

function handleOpen() {
modalRef.value.open(); // 调用子组件暴露的 open 方法
}
</script>

<template>
<button @click="handleOpen">打开弹窗</button>
<ChildModal ref="modalRef">
<p>模态框内容</p>
</ChildModal>
</template>

defineExpose 的设计哲学:在选项式 API 中,父组件通过 this.$refs.xxx 可以访问子组件的所有属性和方法,这破坏了封装性。组合式 API 通过 defineExpose 强制开发者明确声明哪些接口是对外公开的,这是“封装”原则在组件 API 中的体现。父组件只能访问子组件显式暴露的部分,无权触及内部实现细节。


3. nextTick:等待 DOM 更新完成

3.1 问题:响应式数据变化后,DOM 不会立即更新

Vue 的响应式更新是异步批处理的。当你修改一个 ref 的值时,Vue 不会立即更新 DOM,而是将更新推入队列,在当前同步代码执行完毕后统一刷新。这种机制避免了多次数据变更触发多次不必要的 DOM 更新,显著提升了性能。但这也意味着:如果你在修改数据后立即读取 DOM,读到的仍然是旧状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup>
import { ref, nextTick } from 'vue';

const count = ref(0);
const messageRef = ref(null);

async function increment() {
count.value++;
// 此时 DOM 尚未更新!
console.log(messageRef.value.textContent); // 仍然是旧值 '0'

// 等待 DOM 更新完成
await nextTick();
console.log(messageRef.value.textContent); // 新值 '1'
}
</script>

<template>
<p ref="messageRef">{{ count }}</p>
<button @click="increment">+1</button>
</template>

3.2 nextTick 的两种调用方式

1
2
3
4
5
6
7
8
// 回调方式
nextTick(() => {
console.log('DOM 已更新');
});

// async/await 方式(推荐)
await nextTick();
console.log('DOM 已更新');

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.valuenull 变为有效 DOM 元素的最早时机。任何依赖 DOM 存在的初始化逻辑(如初始化滚动条、Canvas、第三方库)都应在此钩子中进行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import { ref, onMounted } from 'vue';

const chartRef = ref(null);

onMounted(() => {
// 此时 chartRef.value 指向真实的 <canvas> 元素
const ctx = chartRef.value.getContext('2d');
ctx.fillStyle = 'green';
ctx.fillRect(10, 10, 100, 100);
});
</script>

<template>
<canvas ref="chartRef" width="200" height="200"></canvas>
</template>

4.2 在 onBeforeUnmount 中清理副作用

手动添加的 DOM 事件监听、定时器、第三方库实例必须在组件卸载前清理,否则会造成内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';

const containerRef = ref(null);
let swiperInstance = null;

onMounted(() => {
// 初始化第三方 Swiper 库
swiperInstance = new Swiper(containerRef.value, { loop: true });
});

onBeforeUnmount(() => {
// 销毁 Swiper 实例
swiperInstance?.destroy();
swiperInstance = null;
});
</script>

<template>
<div ref="containerRef" class="swiper-container">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="i in 5" :key="i">Slide {{ i }}</div>
</div>
</div>
</template>

4.3 在 onUpdated 中响应 DOM 变化

当响应式数据变化导致 DOM 更新后,onUpdated 钩子触发。你可以在此钩子中根据新的 DOM 状态执行操作。但需要注意避免修改响应式数据,否则可能触发无限更新循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { ref, onUpdated } from 'vue';

const messages = ref([]);
const listRef = ref(null);

function addMessage(text) {
messages.value.push({ id: Date.now(), text });
}

onUpdated(() => {
// 每次消息列表更新后自动滚到底部
listRef.value.scrollTop = listRef.value.scrollHeight;
});
</script>

4.4 watch 配合 nextTick:更精确的 DOM 响应

如果你需要在特定数据变化后(而非任何数据变化后)操作 DOM,使用 watch 配合 nextTickonUpdated 更精确。

1
2
3
4
5
6
7
8
const showEditor = ref(false);

watch(showEditor, async (newVal) => {
if (newVal) {
await nextTick();
editorRef.value.focus(); // 编辑器显示后立即聚焦
}
});

5. 综合实战:带聚焦和滚动功能的聊天组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';

// 聊天消息
const messages = ref([
{ id: 1, user: 'Alice', text: '你好!', time: '10:30' },
{ id: 2, user: 'Bob', text: '你好,有什么可以帮助你的?', time: '10:31' },
]);

// 新消息输入
const newMessage = ref('');
const inputRef = ref(null);
const listRef = ref(null);

// 挂载后聚焦输入框
onMounted(() => {
inputRef.value.focus();
});

// 发送消息
async function sendMessage() {
if (!newMessage.value.trim()) return;

messages.value.push({
id: Date.now(),
user: 'Me',
text: newMessage.value,
time: new Date().toLocaleTimeString(),
});
newMessage.value = '';

// 等待 DOM 更新后滚到底部并聚焦
await nextTick();
listRef.value.scrollTop = listRef.value.scrollHeight;
inputRef.value.focus();
}

// 按回车发送
function handleKeydown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}

// 清理
onBeforeUnmount(() => {
console.log('聊天组件即将卸载');
});
</script>

<template>
<div class="chat-container">
<div ref="listRef" class="chat-messages">
<div
v-for="msg in messages"
:key="msg.id"
:class="['message', msg.user === 'Me' ? 'message-self' : 'message-other']"
>
<strong>{{ msg.user }}</strong>
<span class="time">{{ msg.time }}</span>
<p>{{ msg.text }}</p>
</div>
</div>

<div class="chat-input">
<textarea
ref="inputRef"
v-model="newMessage"
@keydown="handleKeydown"
placeholder="输入消息,按 Enter 发送..."
rows="2"
></textarea>
<button @click="sendMessage">发送</button>
</div>
</div>
</template>

<style scoped>
.chat-container {
max-width: 500px;
margin: 0 auto;
border: 1px solid #ddd;
border-radius: 8px;
display: flex;
flex-direction: column;
height: 400px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.message {
margin-bottom: 12px;
}
.message-self {
text-align: right;
}
.time {
font-size: 0.8em;
color: #999;
margin-left: 8px;
}
.chat-input {
display: flex;
gap: 8px;
padding: 12px;
border-top: 1px solid #eee;
}
.chat-input textarea {
flex: 1;
padding: 8px;
border-radius: 4px;
border: 1px solid #ccc;
resize: none;
}
.chat-input button {
padding: 8px 16px;
background: #4f46e5;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>

设计要点

  • onMounted 中聚焦输入框,确保用户可以直接输入。
  • 发送消息后使用 await nextTick() 等待 DOM 更新,然后滚动到底部。
  • 键盘事件使用 @keydown 而非 @keyup,更快速地响应回车键。
  • onBeforeUnmount 中输出日志(实际项目可能清理定时器或事件监听)。
  • 模板引用 listRefinputRef 分别操作滚动容器和输入框。

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) 在 Vue 3 组合式 API 中,最早能安全访问模板引用(ref.value 指向有效 DOM 元素)的钩子是?
    A. onBeforeMount
    B. onMounted
    C. setup 顶层
    D. onBeforeCreate

  2. (单选) 关于 nextTick 的描述,正确的是?
    A. 它会立即重新渲染整个组件。
    B. 它返回一个 Promise,在 DOM 更新队列清空后解析。
    C. 它是同步执行的。
    D. 它仅适用于修改 ref 后的操作,对 reactive 无效。

  3. (填空)<script setup> 中,要让父组件通过模板引用访问子组件的方法,子组件必须使用 ______ 宏显式暴露这些方法。

  4. (多选) 以下哪些操作适合在 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 } 对象。
  • 定义 expandedIndex ref,初始 nullanswerRefs ref,初始 []
  • 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 等待。


课后练习答案

一、概念自测答案

  1. B

    • 解析:onMounted 是组件挂载到 DOM 后触发的钩子,此时模板引用的 DOM 元素已就绪。onBeforeMount 阶段 DOM 尚未生成。
  2. B

    • 解析:nextTick 返回一个 Promise,在 DOM 更新队列清空后解析。A 错误(不立即渲染),C 错误(异步的),D 错误(对 reactive 也有效)。
  3. defineExpose

    • 解析:<script setup> 中的子组件通过 defineExpose({ ... }) 暴露接口给父组件的模板引用。
  4. A、B、D

    • 解析:C 错误,清理定时器应在 onBeforeUnmountonUnmounted 中执行。A(网络请求)、B(初始化第三方库)、D(操作 DOM 元素)均适合 onMounted

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请用 Vue 3 <script setup> 实现一个手风琴 FAQ 组件。要求:

  • FAQ 数据为静态数组,每项有 id、question、answer。
  • 点击问题标题展开/折叠答案(手风琴模式,仅一个展开)。
  • 使用 nextTick 在答案展开后自动滚动到视口中央(scrollIntoView)。
  • 使用 v-for 中的模板引用获取答案区域 DOM。
  • onMounted 自动展开第一个。
  • 输出完整单文件组件,包含必要样式。”
CATALOG
  1. 1. 第105课:生命周期与模板引用——钩子函数、ref 模板引用、nextTick
    1. 1.1. 1. 组合式 API 的生命周期钩子
      1. 1.1.1. 1.1 完整生命周期图示与钩子映射
      2. 1.1.2. 1.2 setup 替代了 beforeCreate 和 created
      3. 1.1.3. 1.3 父子组件生命周期执行顺序
      4. 1.1.4. 1.4 常用钩子场景
    2. 1.2. 2. 模板引用:ref 属性与响应式引用
      1. 1.2.1. 2.1 获取单个 DOM 元素
      2. 1.2.2. 2.2 v-for 中的模板引用:获取元素数组
      3. 1.2.3. 2.3 函数型模板引用
      4. 1.2.4. 2.4 获取子组件实例:defineExpose
    3. 1.3. 3. nextTick:等待 DOM 更新完成
      1. 1.3.1. 3.1 问题:响应式数据变化后,DOM 不会立即更新
      2. 1.3.2. 3.2 nextTick 的两种调用方式
      3. 1.3.3. 3.3 典型场景
    4. 1.4. 4. 生命周期与模板引用的协作模式
      1. 1.4.1. 4.1 在 onMounted 中安全访问模板引用
      2. 1.4.2. 4.2 在 onBeforeUnmount 中清理副作用
      3. 1.4.3. 4.3 在 onUpdated 中响应 DOM 变化
      4. 1.4.4. 4.4 watch 配合 nextTick:更精确的 DOM 响应
    5. 1.5. 5. 综合实战:带聚焦和滚动功能的聊天组件
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.6.3. 三、Agent 模式下的提示词示例
      4. 1.6.4. 四、面试真题与参考答案
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)