模板引用
虽然 Vue 的声明式渲染模型已经帮你抽象掉了大多数直接的 DOM 操作,但在某些情况下,我们仍然需要直接访问底层的 DOM 元素。为此,我们可以使用特殊的 ref 属性:
template
<input ref="input">ref 是一个特殊属性,类似于在 v-for 章节中讨论过的 key 属性。它允许我们在组件挂载后,直接获取某个特定 DOM 元素或子组件实例的引用。例如,当你想在组件挂载时以编程方式聚焦某个输入框,或者在某个元素上初始化第三方库时,这会很有用。
访问 Refs
要使用 Composition API 获取该引用,我们可以使用 useTemplateRef() 辅助函数:
vue
<script setup>
import { useTemplateRef, onMounted } from 'vue'
// 第一个参数必须与模板中的 ref 值匹配
const input = useTemplateRef('my-input')
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="my-input" />
</template>当使用 TypeScript 时,Vue 的 IDE 支持和 vue-tsc 会根据匹配的 ref 属性所绑定的是哪个元素或组件,自动推断 input.value 的类型。
3.5 之前的用法
在 3.5 之前尚未引入 useTemplateRef() 的版本中,我们需要声明一个 ref,其名称与模板中的 ref 属性值相匹配:
vue
<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 用于保存元素引用
// 名称必须与模板 ref 值匹配
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>如果不使用 <script setup>,还要记得从 setup() 中返回这个 ref:
js
export default {
setup() {
const input = ref(null)
// ...
return {
input
}
}
}请注意,你只能在组件挂载之后访问该 ref。如果你尝试在模板表达式中访问 input,它在首次渲染时将是 null。这是因为该元素直到首次渲染后才存在!
如果你想监听模板 ref 的变化,请务必考虑 ref 可能为 null 的情况:
js
watchEffect(() => {
if (input.value) {
input.value.focus()
} else {
// 还未挂载,或者该元素已被卸载(例如通过 v-if)
}
})另请参阅:类型化模板 Refs
组件上的 Ref
本节假设你已经了解 组件。如果愿意,可以先跳过,稍后再回来。
ref 也可以用于子组件。在这种情况下,引用将是一个组件实例:
vue
<script setup>
import { useTemplateRef, onMounted } from 'vue'
import Child from './Child.vue'
const childRef = useTemplateRef('child')
onMounted(() => {
// childRef.value 将持有一个 <Child /> 实例
})
</script>
<template>
<Child ref="child" />
</template>3.5 之前的用法
vue
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const child = ref(null)
onMounted(() => {
// child.value 将持有一个 <Child /> 实例
})
</script>
<template>
<Child ref="child" />
</template>如果子组件使用的是 Options API,或者没有使用 <script setup>,被引用的实例将与子组件的 this 完全相同,这意味着父组件可以完全访问子组件的每个属性和方法。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,因此组件 ref 只应在绝对必要时使用——在大多数情况下,你应该先尝试使用标准的 props 和 emit 接口来实现父子交互。
这里有一个例外:使用 <script setup> 的组件默认是私有的:父组件引用一个使用 <script setup> 的子组件时,除非子组件通过 defineExpose 宏选择暴露一个公共接口,否则父组件无法访问任何内容:
vue
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
// 编译器宏(例如 defineExpose)不需要导入
defineExpose({
a,
b
})
</script>当父组件通过模板 ref 获取该组件实例时,得到的实例形状将是 { a: number, b: number }(refs 会像在普通实例上一样自动解包)。
请注意,defineExpose 必须在任何 await 操作之前调用。否则,在 await 操作之后暴露的属性和方法将无法访问。
另请参阅:类型化组件模板 Refs
v-for 中的 Refs
需要 v3.5 或更高版本
当 ref 用在 v-for 中时,相应的 ref 应该包含一个数组值,挂载后这些元素会被填充进去:
vue
<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'
const list = ref([
/* ... */
])
const itemRefs = useTemplateRef('items')
onMounted(() => console.log(itemRefs.value))
</script>
<template>
<ul>
<li v-for="item in list" ref="items">
{{ item }}
</li>
</ul>
</template>3.5 之前的用法
在 3.5 之前尚未引入 useTemplateRef() 的版本中,我们需要声明一个 ref,其名称与模板中的 ref 属性值相匹配。该 ref 也应包含数组值:
vue
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([
/* ... */
])
const itemRefs = ref([])
onMounted(() => console.log(itemRefs.value))
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>需要注意的是,ref 数组不能保证与源数组具有相同的顺序。
函数 Refs
ref 属性除了可以绑定字符串 key 之外,还可以绑定到一个函数,该函数会在每次组件更新时被调用,并让你能够完全灵活地决定将元素引用存储在哪里。该函数会接收元素引用作为第一个参数:
template
<input :ref="(el) => { /* 将 el 赋值给某个属性或 ref */ }">请注意,我们这里使用的是动态 :ref 绑定,因此可以传入函数而不是 ref 名称字符串。当元素被卸载时,该参数将是 null。当然,你也可以使用一个方法来代替内联函数。