本篇定位 :在 Vue 的声明式渲染体系中,我们通常不需要直接操作 DOM。但在某些场景下(如获取元素尺寸、聚焦输入框、调用子组件方法等),我们仍然需要直接访问 DOM 元素或组件实例。本篇将系统讲解模板引用的各种用法,从基础的
ref到 Vue 3.5+ 的useTemplateRef,再到nextTick的原理与使用。读完本篇,你将能够安全、高效地在 Vue 中操作 DOM 和组件实例。
一、为什么需要模板引用?
1.1 声明式 vs 命令式
Vue 的核心是声明式渲染 ------你描述"界面应该长什么样",Vue 负责更新 DOM。但在某些情况下,命令式操作 DOM 是必要的:
| 场景 | 说明 |
|---|---|
| 聚焦控制 | 页面加载后自动聚焦到输入框 |
| 尺寸测量 | 获取元素的宽高、位置 |
| 第三方库集成 | 图表、地图、编辑器等需要 DOM 元素 |
| 动画控制 | 手动触发动画的开始/暂停 |
| 调用子组件方法 | 父组件需要触发子组件的特定行为 |
模板引用(Template Ref) 就是 Vue 提供的在声明式框架中安全地命令式操作 DOM/组件的机制。
1.2 与响应式 ref 的区别
| 维度 | 响应式 ref | 模板 ref |
|---|---|---|
| 用途 | 存储响应式数据 | 获取 DOM 元素/组件实例 |
| 值 | 任意 JavaScript 值 | DOM 元素或组件实例 |
| 响应式 | ✅ 是 | ❌ 否(只是引用) |
| 声明方式 | const count = ref(0) |
const el = ref(null) + ref="el" |
二、模板 ref 的基本用法
2.1 绑定 DOM 元素
使用 ref 属性标记元素,然后通过同名变量访问:
vue
<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref,名称与模板中的 ref 属性一致
const inputEl = ref<HTMLInputElement | null>(null)
const divEl = ref<HTMLElement | null>(null)
onMounted(() => {
// 在 mounted 之后可以访问 DOM 元素
console.log(inputEl.value) // <input> 元素
console.log(divEl.value) // <div> 元素
// 操作 DOM
inputEl.value?.focus()
console.log('元素宽度:', divEl.value?.offsetWidth)
})
</script>
<template>
<!-- 使用 ref 属性绑定 -->
<input ref="inputEl" placeholder="自动聚焦" />
<div ref="divEl" class="box">内容</div>
</template>
关键规则:
ref属性的值必须与变量名一致- 变量必须使用
ref(null)初始化 - DOM 元素在
onMounted之后才可用(setup执行时 DOM 尚未渲染)
2.2 在 v-for 中使用 ref
在 v-for 中,ref 会返回数组 或对象:
vue
<script setup>
import { ref, onMounted } from 'vue'
const items = ref(['A', 'B', 'C', 'D'])
const itemRefs = ref<HTMLElement[]>([])
onMounted(() => {
console.log(itemRefs.value) // [div, div, div, div]
itemRefs.value.forEach((el, i) => {
console.log(`第 ${i} 个元素的文本:`, el.textContent)
})
})
</script>
<template>
<div
v-for="(item, index) in items"
:key="index"
:ref="(el) => itemRefs[index] = el"
>
{{ item }}
</div>
</template>
使用对象存储 ref(更清晰):
vue
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
{ id: 3, name: 'C' }
])
// 使用对象存储每个元素的引用
const itemRefs = ref<Record<number, HTMLElement>>({})
function setItemRef(el: HTMLElement | null, id: number) {
if (el) {
itemRefs.value[id] = el
}
}
</script>
<template>
<div
v-for="item in items"
:key="item.id"
:ref="(el) => setItemRef(el, item.id)"
>
{{ item.name }}
</div>
</template>
2.3 在条件渲染中使用 ref
条件渲染中的 ref 需要处理元素可能不存在的情况:
vue
<script setup>
import { ref, watch } from 'vue'
const show = ref(false)
const contentEl = ref<HTMLElement | null>(null)
// 监听 show 变化,在元素出现后操作
watch(show, (newVal) => {
if (newVal) {
// 使用 nextTick 确保 DOM 已更新
nextTick(() => {
console.log(contentEl.value) // 元素存在
})
}
})
</script>
<template>
<button @click="show = !show">切换</button>
<div v-if="show" ref="contentEl">条件内容</div>
</template>
三、Vue 3.5+ 新特性:useTemplateRef
3.1 基本用法
Vue 3.5 引入了 useTemplateRef ,提供类型安全的模板引用方式:
vue
<script setup>
import { useTemplateRef, onMounted } from 'vue'
// 类型安全的模板引用
const inputRef = useTemplateRef<HTMLInputElement>('inputEl')
const divRef = useTemplateRef<HTMLElement>('divEl')
onMounted(() => {
// 自动推导类型,无需手动断言
inputRef.value?.focus()
console.log(divRef.value?.offsetWidth)
})
</script>
<template>
<input ref="inputEl" />
<div ref="divEl">内容</div>
</template>
3.2 类型安全与智能提示
useTemplateRef 的核心优势是类型安全:
vue
<script setup lang="ts">
import { useTemplateRef } from 'vue'
// ✅ 类型安全:明确指定元素类型
const inputRef = useTemplateRef<HTMLInputElement>('input')
const buttonRef = useTemplateRef<HTMLButtonElement>('button')
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvas')
function handleClick() {
// IDE 会提供准确的类型提示
inputRef.value?.focus()
inputRef.value?.select()
// ✅ 类型检查:inputRef 是 HTMLInputElement,有 value 属性
console.log(inputRef.value?.value)
// ❌ 类型错误:HTMLButtonElement 没有 value 属性
// console.log(buttonRef.value?.value)
}
</script>
<template>
<input ref="input" placeholder="输入框" />
<button ref="button">按钮</button>
<canvas ref="canvas"></canvas>
</template>
3.3 useTemplateRef vs 传统 ref
| 特性 | 传统 ref | useTemplateRef |
|---|---|---|
| 类型安全 | 需要手动断言 | ✅ 自动推导 |
| 语法简洁 | 需要声明和匹配 | ✅ 更直观 |
| IDE 支持 | 一般 | ✅ 更智能 |
| Vue 版本 | Vue 3.0+ | Vue 3.5+ |
| 推荐度 | 可用 | 推荐 |
四、组件引用与 defineExpose
4.1 引用子组件实例
在父组件中可以通过 ref 获取子组件的实例:
vue
<!-- 父组件 -->
<script setup>
import { useTemplateRef } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childRef = useTemplateRef<InstanceType<typeof ChildComponent>>('child')
function callChildMethod() {
// 调用子组件暴露的方法
childRef.value?.sayHello()
childRef.value?.reset()
}
</script>
<template>
<ChildComponent ref="child" />
<button @click="callChildMethod">调用子组件方法</button>
</template>
4.2 defineExpose:暴露组件 API
默认情况下,子组件的内部数据和方法是私有的 。使用 defineExpose 暴露需要对外公开的 API:
vue
<!-- ChildComponent.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const message = ref('Hello')
function increment() {
count.value++
}
function reset() {
count.value = 0
message.value = 'Hello'
}
function sayHello() {
console.log('Hello from child!', message.value)
}
// 暴露给父组件的 API
defineExpose({
count, // 暴露响应式数据
increment, // 暴露方法
reset,
sayHello
})
</script>
<template>
<div>
<p>子组件计数:{{ count }}</p>
<p>消息:{{ message }}</p>
<button @click="increment">+1</button>
</div>
</template>
父组件调用:
vue
<script setup>
import { useTemplateRef } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childRef = useTemplateRef<InstanceType<typeof ChildComponent>>('child')
function handleReset() {
childRef.value?.reset()
}
function handleIncrement() {
childRef.value?.increment()
}
function logCount() {
// 访问暴露的响应式数据
console.log('子组件 count:', childRef.value?.count)
}
</script>
<template>
<ChildComponent ref="child" />
<button @click="handleIncrement">父组件调用 increment</button>
<button @click="handleReset">父组件调用 reset</button>
<button @click="logCount">查看 count</button>
</template>
4.3 类型安全的 defineExpose
使用 TypeScript 时,可以定义暴露的类型:
typescript
// ChildComponent.vue
export interface ChildExpose {
count: number
increment: () => void
reset: () => void
sayHello: () => void
}
<script setup>
import type { ChildExpose } from './ChildComponent.vue'
// 暴露时自动匹配类型
defineExpose<ChildExpose>({
count,
increment,
reset,
sayHello
})
</script>
父组件使用类型:
vue
<script setup lang="ts">
import ChildComponent, { type ChildExpose } from './ChildComponent.vue'
const childRef = useTemplateRef<ChildExpose>('child')
// 现在 childRef.value 的类型是 ChildExpose | null
</script>
五、nextTick:等待 DOM 更新
5.1 为什么需要 nextTick?
Vue 的 DOM 更新是异步的,数据变化后 DOM 不会立即更新:
vue
<script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
const el = useTemplateRef<HTMLSpanElement>('countEl')
function increment() {
count.value = 1
// ❌ 此时 DOM 尚未更新
console.log(el.value?.textContent) // 仍然是 '0'
// ✅ 使用 nextTick 等待 DOM 更新
nextTick(() => {
console.log(el.value?.textContent) // 现在是 '1'
})
}
</script>
<template>
<span ref="countEl">{{ count }}</span>
<button @click="increment">增加</button>
</template>
5.2 nextTick 的原理
Vue 的更新机制:
数据变化 → 收集依赖 → 放入微任务队列 → 批量执行 → DOM 更新 → nextTick 回调
组件数据变化
↓
Vue 将更新放入微任务队列
↓
队列中的更新合并后执行(批量更新)
↓
DOM 更新完成
↓
nextTick 回调执行
5.3 nextTick 的用法
方式一:回调方式:
vue
<script setup>
import { nextTick } from 'vue'
function updateAndMeasure() {
// 更新数据
data.value = 'new value'
// 等待 DOM 更新后测量
nextTick(() => {
const height = containerRef.value?.offsetHeight
console.log('容器高度:', height)
})
}
</script>
方式二:Promise 方式(推荐):
vue
<script setup>
import { nextTick } from 'vue'
async function updateAndMeasure() {
// 更新数据
data.value = 'new value'
// 等待 DOM 更新
await nextTick()
// 现在可以安全地操作 DOM
const height = containerRef.value?.offsetHeight
console.log('容器高度:', height)
}
</script>
5.4 nextTick 的常见使用场景
① 获取更新后的 DOM 尺寸:
vue
<script setup>
import { ref, nextTick } from 'vue'
const isExpanded = ref(false)
const contentRef = useTemplateRef<HTMLElement>('content')
async function toggleExpand() {
isExpanded.value = !isExpanded.value
// 等待 DOM 展开后获取高度
await nextTick()
const height = contentRef.value?.scrollHeight
console.log('展开后高度:', height)
}
</script>
② 聚焦输入框:
vue
<script setup>
import { ref, nextTick } from 'vue'
const showInput = ref(false)
const inputRef = useTemplateRef<HTMLInputElement>('input')
async function showAndFocus() {
showInput.value = true
await nextTick()
inputRef.value?.focus()
}
</script>
③ 滚动到指定元素:
vue
<script setup>
import { ref, nextTick } from 'vue'
const messages = ref(['msg1', 'msg2', 'msg3'])
const lastMsgRef = useTemplateRef<HTMLElement>('lastMsg')
async function addMessage(text: string) {
messages.value.push(text)
await nextTick()
// 滚动到最新消息
lastMsgRef.value?.scrollIntoView({ behavior: 'smooth' })
}
</script>
5.5 flush 选项:控制更新时机
nextTick 默认在 DOM 更新后执行。Vue 3.5+ 提供了更精细的控制:
vue
<script setup>
import { watch, nextTick } from 'vue'
// 使用 flush: 'post' 的 watch 效果类似 nextTick
watch(count, async (newVal) => {
// DOM 已更新
await nextTick()
// 相当于 flush: 'post'
}, { flush: 'post' })
// 或者在 watch 中直接使用 nextTick
watch(count, async () => {
// 使用 nextTick 确保 DOM 更新
await nextTick()
// 操作 DOM
})
</script>
六、综合实战:可编辑列表组件
让我们综合运用模板 ref、组件引用和 nextTick,构建一个功能完整的可编辑列表:
vue
<!-- EditableList.vue - 主组件 -->
<script setup lang="ts">
import { ref, nextTick, useTemplateRef, onMounted } from 'vue'
import EditableItem from './EditableItem.vue'
// 列表数据
const items = ref([
{ id: 1, text: '学习 Vue 模板引用', completed: false },
{ id: 2, text: '理解 nextTick 原理', completed: false },
{ id: 3, text: '掌握 defineExpose', completed: false }
])
// 引用
const listContainer = useTemplateRef<HTMLElement>('listContainer')
const newInputRef = useTemplateRef<HTMLInputElement>('newInput')
const itemRefs = ref<Record<number, InstanceType<typeof EditableItem>>>({})
// 新项输入
const newText = ref('')
const isAdding = ref(false)
// 添加新项
async function addItem() {
const text = newText.value.trim()
if (!text) return
isAdding.value = true
const newItem = {
id: Date.now(),
text,
completed: false
}
items.value.unshift(newItem)
newText.value = ''
// 滚动到列表顶部
await nextTick()
listContainer.value?.scrollTo({ top: 0, behavior: 'smooth' })
isAdding.value = false
}
// 删除项
function deleteItem(id: number) {
items.value = items.value.filter(item => item.id !== id)
delete itemRefs.value[id]
}
// 切换完成状态
function toggleItem(id: number) {
const item = items.value.find(i => i.id === id)
if (item) {
item.completed = !item.completed
}
}
// 编辑项
function editItem(id: number) {
// 聚焦到该编辑项
itemRefs.value[id]?.focus()
}
// 滚动到指定项
async function scrollToItem(id: number) {
const ref = itemRefs.value[id]
if (ref) {
// 获取元素的 DOM 引用(需要子组件暴露)
// 实现见下方 EditableItem
}
}
// 统计信息
const stats = computed(() => ({
total: items.value.length,
completed: items.value.filter(i => i.completed).length,
active: items.value.filter(i => !i.completed).length
}))
// 清空已完成
function clearCompleted() {
items.value = items.value.filter(i => !i.completed)
}
// 自动聚焦新输入框
onMounted(() => {
newInputRef.value?.focus()
})
// 暴露给父组件的方法
defineExpose({
addItem,
clearCompleted,
getItems: () => items.value
})
</script>
<template>
<div class="editable-list">
<h2>📋 可编辑列表</h2>
<!-- 统计 -->
<div class="stats">
<span>总计:{{ stats.total }}</span>
<span>已完成:{{ stats.completed }}</span>
<span>待完成:{{ stats.active }}</span>
<button v-if="stats.completed > 0" @click="clearCompleted" class="btn-clear">
清空已完成
</button>
</div>
<!-- 添加表单 -->
<div class="add-form">
<input
ref="newInput"
v-model="newText"
@keydown.enter="addItem"
placeholder="输入新待办..."
class="add-input"
:disabled="isAdding"
/>
<button @click="addItem" :disabled="isAdding" class="btn-add">
{{ isAdding ? '⏳ 添加中...' : '➕ 添加' }}
</button>
</div>
<!-- 列表 -->
<div ref="listContainer" class="list-container">
<EditableItem
v-for="item in items"
:key="item.id"
:item="item"
ref="itemRefs"
@delete="deleteItem"
@toggle="toggleItem"
@edit="editItem"
/>
</div>
</div>
</template>
<style scoped>
.editable-list {
max-width: 700px;
margin: 20px auto;
padding: 24px;
background: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.06);
}
h2 { margin-top: 0; font-size: 22px; }
.stats {
display: flex;
gap: 20px;
padding: 10px 16px;
background: #f7fafc;
border-radius: 8px;
font-size: 14px;
color: #4a5568;
}
.btn-clear {
margin-left: auto;
background: #fed7d7;
color: #c53030;
border: none;
padding: 2px 14px;
border-radius: 4px;
cursor: pointer;
}
.add-form {
display: flex;
gap: 10px;
margin: 16px 0;
}
.add-input {
flex: 1;
padding: 10px 14px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 15px;
}
.add-input:focus {
outline: none;
border-color: #4299e1;
}
.btn-add {
padding: 10px 24px;
background: #4299e1;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
}
.btn-add:disabled { opacity: 0.5; cursor: not-allowed; }
.list-container {
max-height: 500px;
overflow-y: auto;
}
</style>
vue
<!-- EditableItem.vue - 列表项子组件 -->
<script setup lang="ts">
import { ref, nextTick, useTemplateRef, computed } from 'vue'
interface Item {
id: number
text: string
completed: boolean
}
const props = defineProps<{
item: Item
}>()
const emit = defineEmits<{
(e: 'delete', id: number): void
(e: 'toggle', id: number): void
(e: 'edit', id: number): void
}>()
// 编辑状态
const isEditing = ref(false)
const editText = ref(props.item.text)
// 引用
const textDisplayRef = useTemplateRef<HTMLSpanElement>('textDisplay')
const editInputRef = useTemplateRef<HTMLInputElement>('editInput')
const itemRef = useTemplateRef<HTMLElement>('item')
// 开始编辑
function startEdit() {
isEditing.value = true
editText.value = props.item.text
// 聚焦到输入框
nextTick(() => {
editInputRef.value?.focus()
editInputRef.value?.select()
})
emit('edit', props.item.id)
}
// 保存编辑
function saveEdit() {
const text = editText.value.trim()
if (text) {
// 更新本地文本(通过父组件更新)
emit('edit', props.item.id)
// 实际项目中需要通过父组件更新
}
isEditing.value = false
}
// 取消编辑
function cancelEdit() {
isEditing.value = false
editText.value = props.item.text
}
// 键盘事件
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
saveEdit()
} else if (event.key === 'Escape') {
cancelEdit()
}
}
// 暴露给父组件的方法
defineExpose({
focus: () => {
if (isEditing.value) {
editInputRef.value?.focus()
} else {
startEdit()
}
},
getElement: () => itemRef.value,
getText: () => props.item.text
})
// 样式类
const itemClass = computed(() => ({
'item': true,
'completed': props.item.completed,
'editing': isEditing.value
}))
</script>
<template>
<div ref="item" :class="itemClass">
<!-- 复选框 -->
<input
type="checkbox"
:checked="item.completed"
@change="emit('toggle', item.id)"
class="item-checkbox"
/>
<!-- 显示模式 -->
<template v-if="!isEditing">
<span ref="textDisplay" class="item-text">{{ item.text }}</span>
<div class="item-actions">
<button @click="startEdit" class="btn-edit">✏️</button>
<button @click="emit('delete', item.id)" class="btn-delete">🗑️</button>
</div>
</template>
<!-- 编辑模式 -->
<template v-else>
<input
ref="editInput"
v-model="editText"
@keydown="handleKeydown"
@blur="saveEdit"
class="edit-input"
/>
<div class="edit-actions">
<button @click="saveEdit" class="btn-save">✅</button>
<button @click="cancelEdit" class="btn-cancel">❌</button>
</div>
</template>
</div>
</template>
<style scoped>
.item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #f7fafc;
border-radius: 8px;
margin-bottom: 8px;
transition: all 0.2s;
}
.item:hover { background: #edf2f7; }
.item.completed { opacity: 0.6; }
.item.completed .item-text {
text-decoration: line-through;
color: #a0aec0;
}
.item.editing {
background: #ebf8ff;
border: 2px solid #4299e1;
}
.item-checkbox {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #4299e1;
flex-shrink: 0;
}
.item-text {
flex: 1;
font-size: 15px;
color: #2d3748;
}
.item-actions {
display: flex;
gap: 4px;
}
.item-actions button {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
padding: 2px 6px;
border-radius: 4px;
transition: background 0.2s;
}
.item-actions .btn-edit:hover { background: #ebf8ff; }
.item-actions .btn-delete:hover { background: #fff5f5; }
.edit-input {
flex: 1;
padding: 4px 10px;
border: 2px solid #4299e1;
border-radius: 6px;
font-size: 15px;
}
.edit-input:focus { outline: none; }
.edit-actions {
display: flex;
gap: 4px;
}
.edit-actions button {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
padding: 2px 6px;
border-radius: 4px;
}
.edit-actions .btn-save:hover { background: #f0fff4; }
.edit-actions .btn-cancel:hover { background: #fff5f5; }
</style>
七、模板引用的最佳实践
7.1 使用 useTemplateRef(Vue 3.5+)
typescript
// ✅ 推荐:类型安全
const inputRef = useTemplateRef<HTMLInputElement>('input')
// ⚠️ 可用但类型不安全
const inputRef = ref<HTMLInputElement | null>(null)
7.2 在 nextTick 中访问 DOM
typescript
// ✅ 推荐
async function updateAndAccess() {
data.value = 'new'
await nextTick()
// 此时 DOM 已更新
console.log(el.value?.textContent)
}
// ❌ 不推荐(可能获取到旧值)
function updateAndAccess() {
data.value = 'new'
console.log(el.value?.textContent) // 可能是旧值
}
7.3 条件渲染中的 ref
typescript
// ✅ 推荐:使用 watch + nextTick
watch(show, async (newVal) => {
if (newVal) {
await nextTick()
el.value?.focus()
}
})
// ✅ 推荐:在 onUpdated 中处理(注意避免循环)
onUpdated(() => {
if (show.value && !hasFocused.value) {
el.value?.focus()
hasFocused.value = true
}
})
7.4 暴露合理的 API
typescript
// ✅ 推荐:只暴露必要的方法和数据
defineExpose({
reset,
validate,
data: readonly(data) // 使用 readonly 保护数据
})
// ❌ 不推荐:暴露过多内部实现
defineExpose({
internalState,
privateMethod,
_tempData
})
7.5 清理 DOM 引用
typescript
// ✅ 推荐:在组件卸载时清理
onUnmounted(() => {
// 清除外部引用
if (chartInstance) {
chartInstance.destroy()
chartInstance = null
}
})
八、常见陷阱与解决方案
8.1 ref 在 setup 中访问为 null
vue
<script setup>
import { ref, onMounted } from 'vue'
const el = ref<HTMLElement | null>(null)
// ❌ 错误:setup 时 DOM 尚未渲染
console.log(el.value) // null
// ✅ 正确:在 onMounted 中访问
onMounted(() => {
console.log(el.value) // DOM 元素
})
</script>
8.2 v-for 中的 ref 顺序问题
vue
<script setup>
const items = ref(['A', 'B', 'C'])
const itemRefs = ref<HTMLElement[]>([])
// ❌ 错误:数组顺序可能与渲染顺序不一致
// 使用函数式 ref 确保顺序正确
</script>
<template>
<!-- ✅ 正确:使用函数式 ref -->
<div
v-for="(item, index) in items"
:key="index"
:ref="(el) => itemRefs[index] = el"
>
{{ item }}
</div>
</template>
8.3 组件引用未暴露方法
vue
<!-- ❌ 子组件未暴露方法 -->
<script setup>
// 没有 defineExpose
</script>
<!-- 父组件调用会报错 -->
<script setup>
const childRef = ref(null)
function callChild() {
childRef.value?.someMethod() // undefined
}
</script>
解决方案 :在子组件中使用 defineExpose。
8.4 ref 与响应式数据混淆
vue
<script setup>
// ❌ 错误:模板 ref 和响应式 ref 概念混淆
const input = ref('') // 这是响应式数据
// 模板中 <input ref="input" /> 会冲突
</script>
<template>
<!-- 模板 ref 应该用不同的名字 -->
<input ref="inputRef" v-model="input" />
</template>
九、本篇小结
| 知识点 | 核心内容 | 关键要点 |
|---|---|---|
| 模板 ref | 获取 DOM 元素引用 | 在 onMounted 后访问 |
| useTemplateRef | Vue 3.5+ 类型安全引用 | 推荐使用 |
| 组件引用 | 获取子组件实例 | 需要 defineExpose 暴露 API |
| defineExpose | 暴露组件内部 API | 只暴露必要的 |
| nextTick | 等待 DOM 更新 | 用于数据变化后的 DOM 操作 |
| v-for ref | 使用函数式 ref | 保证顺序正确 |
| 清理 | 在 onUnmounted 中清理 |
防止内存泄漏 |
十、思考与练习
-
基础练习 :创建一个
FocusableInput组件,使用useTemplateRef获取输入框引用,暴露focus和select方法给父组件。 -
实践任务 :在可编辑列表组件中,添加"滚动到指定项"功能,点击按钮时列表滚动到对应项的位置(使用
scrollIntoView)。 -
陷阱排查:分析以下代码的问题并修正:
vue<script setup> import { ref } from 'vue' const el = ref<HTMLElement | null>(null) console.log(el.value?.offsetWidth) </script> <template> <div ref="el">内容</div> </template> -
深度思考 :
nextTick和setTimeout(fn, 0)有什么区别?为什么 Vue 选择使用微任务(microtask)而不是宏任务(macrotask)? -
扩展任务:为可编辑列表添加"批量操作"功能,使用模板 ref 获取所有项的 DOM 引用,实现全选、批量删除、批量完成等操作。
下篇预告
第 18 篇我们将学习Vue 2 到 Vue 3 迁移指南,系统讲解:
- 破坏性变更清单(全局 API、v-model、$attrs 等)
- 迁移构建工具使用
- 选项式 API 到组合式 API 的迁移策略
- Vue 2 项目迁移实操
准备好升级你的 Vue 项目到最新版本!🚀