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. 你将学到


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 后:


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>
逻辑代码 69 行(超过 40 行限制,仅展示)

▶ 示例: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 加类型?
AuseTemplateRef<HTMLInputElement>('usernameInput')。或 ref<HTMLInputElement | null>(null)

📖 小节


📝 作业

  1. 基础题(难度⭐) 实现一个简单的自动聚焦表单:

    • 1 个 input + 1 个 button
    • 页面加载后 input 自动 focus
    • 点击 button 后 button 文字改为 "Submitted" 并 disable
  2. 进阶题(难度⭐⭐) 实现一个自动滚动到底部的聊天框:

    • 消息列表(div 容器)
    • 输入框 + 发送按钮
    • 发送后消息添加到列表,自动滚动到底
    • v-for 渲染消息,ref 数组管理 DOM
  3. 挑战题(难度⭐⭐⭐) 实现完整的"父子组件 + Template Refs"系统:

    1. FormValidator 子组件:暴露 validate() 和 reset() 方法
    2. LoginPage 父组件:调用 validate 校验,失败显示错误
    3. 5 个 input 字段(username/email/password/phone/captcha)
    4. useTemplateRef(Vue 3.5+)+ TypeScript 强类型
    5. 弹窗打开时自动 focus 第一个 input
    6. 失败时 focus 到第一个错误字段
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏