Vue.js: Refs 与 DOM 操作
最后更新:2026-08-26
Template Refs 让你直接访问 DOM 元素或子组件实例——比如调用 input.focus()、访问组件方法。Vue 3.5 引入了更强大的 useTemplateRef 组合式 API,配合 TypeScript 类型推断。
Template Refs 是"应急通道"——Vue 推荐用 ref/reactive/props/emit 解决大部分问题,Template Refs 只在需要直接操作 DOM 或组件实例时用。
1. 你将学到
ref="el"模板引用基础useTemplateRef(Vue 3.5+ 推荐)- 访问 DOM 元素(focus、scrollIntoView 等)
- 访问子组件实例(defineExpose 暴露方法)
$refs替代与 Composition API 写法- v-for 中 ref 数组
- 5 大使用场景和 4 个反模式
2. 一个登录表单的"自动聚焦"难题
(1) 痛点:modal 打开后 input 怎么自动 focus?
Alice 构建了一个登录弹窗,需要自动聚焦用户名输入框:
JS
// ❌ 翻车版:直接 querySelector
onMounted(() => {
const input = document.querySelector('.username-input')
input.focus() // ❌ 不符合 Vue 理念
})
Vue 理念:避免直接操作 DOM。改用 Template Refs。
产品经理 Charlie:
"Alice,弹窗打开后,用户名输入框应该自动聚焦,这样用户就能直接开始输入。"
(2) Vue Template Refs 解法
VUE
<template>
<!-- ref="usernameInput" 标记这个元素 -->
<input ref="usernameInput" type="text" class="username-input">
</template>
<script setup>
const { ref, onMounted } = Vue
// 1. 创建 ref 变量(名字必须和模板 ref 一致)
const usernameInput = ref(null)
onMounted(() => {
// 2. DOM 已就绪,访问 input 元素
usernameInput.value.focus()
})
</script>
VUE
<!-- 弹窗场景:点击按钮打开 modal,自动 focus -->
<template>
<button @click="showModal = true">Login</button>
<Modal v-if="showModal" @close="showModal = false">
<input ref="usernameInput" type="text">
</Modal>
</template>
<script setup>
const { ref, nextTick } = Vue
const showModal = ref(false)
const usernameInput = ref(null)
async function openModal() {
showModal.value = true
// ✅ 等 DOM 更新后再 focus
await nextTick()
usernameInput.value.focus()
}
</script>
(3) 收益
使用 Template Refs 后:
- 代码可读性:明确"我要 ref 这个元素"
- 符合 Vue 理念:不直接操作 DOM
- 类型安全:TypeScript 推断元素类型
- 生命周期正确:在 onMounted 或 nextTick 中访问
3. ref 基础用法
(1) 字符串 ref(Vue 2 风格,已废弃)
JS
// ❌ 不推荐:字符串 ref
export default {
mounted() {
this.$refs.input.focus()
}
}
(2) 变量 ref(Vue 3 推荐)
VUE
<template>
<input ref="inputRef">
</template>
<script setup>
const { ref, onMounted } = Vue
const inputRef = ref(null)
onMounted(() => {
console.log(inputRef.value) // <input> DOM 元素
inputRef.value.focus() // 调用 DOM API
})
</script>
(3) 5 大基本操作
JS
// 1. 访问 DOM 元素
inputRef.value // <input> 元素
// 2. 调用 DOM API
inputRef.value.focus()
inputRef.value.blur()
inputRef.value.select()
inputRef.value.scrollIntoView()
// 3. 读取/修改 DOM 属性
inputRef.value.value // input 的 value
inputRef.value.disabled // disabled 属性
inputRef.value.style.color = 'red' // 修改样式
// 4. 监听 DOM 事件(不推荐,用 @event)
inputRef.value.addEventListener('focus', handler)
// 5. 访问子组件实例(详见 17.4)
childRef.value.someMethod()
4. useTemplateRef(Vue 3.5+ 推荐)
(1) 为什么需要 useTemplateRef?
变量 ref 在 <script setup> 中需要起 2 个名字(模板 ref + 变量),容易不一致。useTemplateRef 用一个名字搞定,TypeScript 推断更强。
VUE
<template>
<input ref="usernameInput">
</template>
<script setup>
const { useTemplateRef, onMounted } = Vue
// ✅ 一个名字搞定(Vue 3.5+)
const inputRef = useTemplateRef('usernameInput')
onMounted(() => {
inputRef.value.focus() // 类型自动推断为 HTMLInputElement
})
</script>
(2) 5 大优势
| 优势 | 说明 |
|---|---|
| 类型推断 | TypeScript 自动知道是 HTMLInputElement |
| 重命名安全 | 改一处名字 IDE 同步 |
| 避免名字错配 | 模板 ref 和变量名不一致会报错 |
| 简化 setup | 无需 const inputRef = ref(null) |
| 更好的 DevTools | Vue DevTools 5.x 支持 |
(3) 完整对比
VUE
<!-- 旧写法:变量 ref -->
<template>
<input ref="usernameInput">
</template>
<script setup>
const { ref, onMounted } = Vue
const inputRef = ref(null) // 名字可能和模板不一致
onMounted(() => inputRef.value.focus())
</script>
<!-- 新写法:useTemplateRef(Vue 3.5+ 推荐)-->
<template>
<input ref="usernameInput">
</template>
<script setup>
const { useTemplateRef, onMounted } = Vue
const inputRef = useTemplateRef('usernameInput') // 一个名字搞定
onMounted(() => inputRef.value.focus())
</script>
5. 访问子组件实例
(1) defineExpose 暴露方法
VUE
<!-- 子组件:MyInput.vue -->
<template>
<input ref="inputRef" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>
<script setup>
const { ref } = Vue
const props = defineProps({ modelValue: String })
const inputRef = ref(null)
// 暴露给父组件使用
defineExpose({
focus: () => inputRef.value?.focus(),
select: () => inputRef.value?.select(),
clear: () => { inputRef.value.value = '' }
})
</script>
(2) 父组件访问
VUE
<template>
<MyInput ref="myInputRef" v-model="searchQuery" />
<button @click="focusInput">Focus Input</button>
</template>
<script setup>
const { ref } = Vue
import MyInput from './MyInput.vue'
const searchQuery = ref('')
const myInputRef = ref(null)
function focusInput() {
myInputRef.value.focus() // 调用子组件暴露的方法
myInputRef.value.select() // 也可以链式调用
}
</script>
(3) 5 大使用场景
| 场景 | 子组件暴露 | 父组件调用 |
|---|---|---|
| 表单聚焦 | focus() |
inputRef.focus() |
| 清空表单 | clear() |
formRef.clear() |
| 重新加载数据 | reload() |
tableRef.reload() |
| 打开弹窗 | open() |
modalRef.open() |
| 提交表单 | submit() |
formRef.submit() |
6. v-for 中的 ref 数组
(1) 基础用法
VUE
<template>
<ul>
<!-- v-for 中 ref 自动收集到数组 -->
<li v-for="item in items" :key="item.id" ref="itemRefs">
{{ item.name }}
</li>
</ul>
</template>
<script setup>
const { ref, onMounted } = Vue
const items = ref([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' }
])
// ✅ 数组形式
const itemRefs = ref([])
onMounted(() => {
// 访问第 2 项的 DOM
itemRefs.value[1].style.color = 'red'
})
</script>
(2) 动态 ref(v-for 动态数量)
VUE
<template>
<button v-for="i in count" :key="i" :ref="el => buttonRefs[i] = el">
Button {{ i }}
</button>
</template>
<script setup>
const { ref, onMounted } = Vue
const count = ref(3)
const buttonRefs = ref({})
onMounted(() => {
// buttonRefs[0] = 第 1 个 button
// buttonRefs[1] = 第 2 个 button
console.log(buttonRefs.value[0])
})
</script>
(3) 5 大注意事项
| 注意点 | 说明 |
|---|---|
| 数组顺序 | 与 v-for 数据顺序一致 |
| 响应式 | ref 数组变化需要 watch |
| 条件渲染 | v-if 后 ref 可能不更新 |
| 动态数量 | 用对象或函数式 ref |
| 性能 | 大量 ref(100+)会拖慢渲染 |
7. 完整示例:5 大实战场景
▶ 示例:登录表单自动聚焦
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
input { padding: 6px; border: 1px solid #ddd; border-radius: 4px; margin: 4px; }
button { padding: 6px 12px; background: #42b883; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
<div id="app">
<form @submit.prevent="handleLogin">
<input ref="usernameRef" v-model="username" placeholder="Username">
<input ref="passwordRef" v-model="password" type="password" placeholder="Password">
<button>Login</button>
</form>
<p style="color: #999; font-size: 0.85rem;">打开页面自动聚焦到 Username 输入框</p>
</div>
<script>
const { createApp, ref, onMounted } = Vue
const App = {
setup() {
const usernameRef = ref(null)
const passwordRef = ref(null)
const username = ref('')
const password = ref('')
onMounted(() => {
// ✅ 自动聚焦到 username 输入框
usernameRef.value.focus()
})
function handleLogin() {
console.log('Login:', username.value, password.value)
}
return { usernameRef, passwordRef, username, password, handleLogin }
}
}
createApp(App).mount('#app')
</script>
▶ 示例:自动滚动到底部(聊天框)
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
.messages { height: 150px; overflow-y: auto; border: 1px solid #ddd; padding: 0.5rem; margin: 0.5rem 0; }
.msg { padding: 4px 8px; background: #f0f9ff; margin: 4px 0; border-radius: 4px; }
input { padding: 6px; border: 1px solid #ddd; border-radius: 4px; }
button { padding: 6px 12px; background: #42b883; color: white; border: none; border-radius: 4px; cursor: pointer; margin-left: 4px; }
</style>
<div id="app">
<div ref="messagesRef" class="messages">
<div v-for="msg in messages" :key="msg.id" class="msg">{{ msg.text }}</div>
</div>
<input v-model="newMessage" @keyup.enter="sendMessage" placeholder="输入消息...">
<button @click="sendMessage">发送</button>
</div>
<script>
const { createApp, ref, nextTick } = Vue
const App = {
setup() {
const messages = ref([
{ id: 1, text: '👋 欢迎来到聊天室' },
{ id: 2, text: '这是历史消息 2' },
{ id: 3, text: '这是历史消息 3' }
])
const newMessage = ref('')
const messagesRef = ref(null)
async function sendMessage() {
if (!newMessage.value.trim()) return
messages.value.push({ id: Date.now(), text: newMessage.value })
newMessage.value = ''
// ✅ 等 DOM 更新后滚动到底部
await nextTick()
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
}
return { messages, newMessage, messagesRef, sendMessage }
}
}
createApp(App).mount('#app')
</script>
▶ 示例:父组件调用子组件方法(defineExpose)
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
input { padding: 6px; border: 1px solid #ddd; border-radius: 4px; margin: 4px 0; display: block; }
button { padding: 6px 12px; margin: 4px; background: #42b883; color: white; border: none; border-radius: 4px; cursor: pointer; }
.error { color: #ef4444; padding: 4px; }
</style>
<div id="app">
<form-validator ref="formRef"></form-validator>
<button @click="submit">Submit</button>
<p v-if="errorMsg" class="error">{{ errorMsg }}</p>
<p v-else-if="successMsg" style="color: #10b981;">{{ successMsg }}</p>
</div>
<script>
const { createApp, ref } = Vue
// 子组件:FormValidator
const FormValidator = {
setup() {
const email = ref('')
const password = ref('')
const emailRef = ref(null)
const passwordRef = ref(null)
// ✅ expose 给父组件
const exposed = {
validate: () => {
if (!email.value || !email.value.includes('@')) {
emailRef.value?.focus()
return { ok: false, field: 'email' }
}
if (!password.value || password.value.length < 6) {
passwordRef.value?.focus()
return { ok: false, field: 'password' }
}
return { ok: true }
},
reset: () => {
email.value = ''
password.value = ''
}
}
// 暴露 validate / reset
if (typeof defineExpose === 'function') defineExpose(exposed)
Object.assign(FormValidator, exposed)
return { email, password, emailRef, passwordRef }
},
template: `
<form>
<input ref="emailRef" v-model="email" placeholder="Email">
<input ref="passwordRef" v-model="password" type="password" placeholder="Password (≥6位)">
</form>
`
}
// 父组件:App
const App = {
components: { FormValidator },
setup() {
const formRef = ref(null)
const errorMsg = ref('')
const successMsg = ref('')
function submit() {
const result = formRef.value.validate()
if (result.ok) {
errorMsg.value = ''
successMsg.value = '✅ Validation passed!'
} else {
successMsg.value = ''
errorMsg.value = '❌ Invalid ' + result.field
}
}
return { formRef, errorMsg, successMsg, submit }
}
}
createApp(App).mount('#app')
</script>
▶ 示例:5 个常见错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
| 模板 ref 拼错 | ref.value 是 null | 检查 ref 名字一致 |
| 在 setup 顶层访问 | ref.value 是 null | 用 onMounted |
| 弹窗内访问 | 不工作 | 用 nextTick 等 DOM |
| v-for ref 数组访问 | 索引错位 | 用 :key 稳定顺序 |
| 跨组件 ref | undefined | 子组件用 defineExpose |
▶ 示例:5 大 ref 类型对比
| 类型 | 例子 | 适用 |
|---|---|---|
| DOM 元素 | ref="inputRef" → HTMLInputElement |
访问 input/div |
| 组件实例 | ref="childRef" → 组件实例 |
调子组件方法 |
| v-for 数组 | ref="itemRefs" → Array |
列表项访问 |
| 函数式 ref | :ref="el => ..." → 单个元素 |
动态数量 |
| 字符串 ref | ref="name" → this.$refs |
Vue 2 风格(不推荐) |
❓ 常见问题
Q 什么时候用 Template Refs?
A 4 种情况:(1) DOM 操作(focus/scroll/canvas);(2) 调子组件方法;(3) 集成第三方库(ECharts/Mapbox);(4) 测量元素大小。其他场景用 ref/reactive/props。
Q useTemplateRef 必须在 Vue 3.5+ 用吗?
A 是的。Vue 3.5+ 才支持。Vue 3.4 及以下用变量 ref。TypeScript 类型推断更优。
Q Template Refs 和 useRef(React)区别?
A React useRef 返回 mutable ref,Vue 模板 ref 返回 ref 对象。Vue 3.5+ useTemplateRef 更接近 React useRef 的 API。
Q 子组件需要 expose 哪些方法?
A 只 expose 父组件真的需要的方法(如 focus / clear / validate)。其他内部方法不要 expose(封装原则)。
Q v-for 中的 ref 数组什么时候更新?
A 每次 v-for 重新渲染时更新。v-if 切换也会更新。watch ref 数组可以响应变化。
Q Template Refs 和 provide/inject 冲突吗?
A 不冲突。Template Refs 用于"父访问子",provide/inject 用于"跨层级共享数据"。可组合使用。
Q 怎么在 TypeScript 中给 ref 加类型?
A 用
useTemplateRef<HTMLInputElement>('usernameInput')。或 ref<HTMLInputElement | null>(null)。📖 小节
- Template Refs 用于直接访问 DOM 元素或子组件实例
- 3 种写法:变量 ref(Vue 3)/ useTemplateRef(Vue 3.5+ 推荐)/ 字符串 ref(Vue 2 风格,不推荐)
- 5 大基本操作:focus / scroll / select / 改属性 / 调子组件
- defineExpose 让子组件暴露方法给父
- v-for 中 ref 自动收集到数组
- 5 大场景:自动聚焦 / 滚动到底 / 调子方法 / 集成第三方 / 测元素大小
- 4 个反模式:拼错 / 顶层访问 / 弹窗不用 nextTick / 跨组件 ref
📝 作业
-
基础题(难度⭐) 实现一个简单的自动聚焦表单:
- 1 个 input + 1 个 button
- 页面加载后 input 自动 focus
- 点击 button 后 button 文字改为 "Submitted" 并 disable
-
进阶题(难度⭐⭐) 实现一个自动滚动到底部的聊天框:
- 消息列表(div 容器)
- 输入框 + 发送按钮
- 发送后消息添加到列表,自动滚动到底
- v-for 渲染消息,ref 数组管理 DOM
-
挑战题(难度⭐⭐⭐) 实现完整的"父子组件 + Template Refs"系统:
- FormValidator 子组件:暴露 validate() 和 reset() 方法
- LoginPage 父组件:调用 validate 校验,失败显示错误
- 5 个 input 字段(username/email/password/phone/captcha)
- useTemplateRef(Vue 3.5+)+ TypeScript 强类型
- 弹窗打开时自动 focus 第一个 input
- 失败时 focus 到第一个错误字段