Vue 3 中模板 ref 的声明方式
虽然这种方式仍然可用,但Vue 3更推荐使用组合式API,它能提供更好的类型推断和逻辑复用能力。
### 组合式 API 中声明模板 ref
组合式API要求你显式声明一个与模板中 `ref` 属性同名的响应式引用。这种引用由 `ref()` 函数创建,初始值通常设为 `null`。
```html
<template>
<input ref="myInput" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
const myInput = ref(null)
onMounted(() => {
myInput.value.focus() // 注意 .value
})
</script>
声明步骤解析
- 导入并调用
ref():创建一个值为null的响应式引用。 - 在模板中使用
ref属性:将属性值设为与响应式引用相同的名称。 - 在生命周期钩子中访问:组件挂载后,
ref对象的值会被自动赋值为对应的DOM元素或子组件实例。注意,在<script setup>或setup()函数中访问时需通过.value。
在模板中绑定 ref 属性
ref 属性的值必须是一个字符串常量,或者是绑定一个动态名称(使用 :ref)。当使用字符串常量时,它必须与 setup 中声明的引用变量名完全相同。Vue 会在挂载时自动完成赋值。
<!-- 静态 ref:引用名称必须与变量 myInput 匹配 -->
<input ref="myInput" />
<!-- 动态 ref:绑定一个字符串变量 -->
<input :ref="elementName" />
对于动态 ref,你需要提前声明一个响应式变量来存储将要绑定的字符串名称:
<script setup>
import { ref } from 'vue'
const elementName = ref('myInput')
// 引用的响应式变量也要命名为 myInput
const myInput = ref(null)
</script>
<template>
<input :ref="elementName" />
</template>
注意:动态 ref 绑定的字符串值改变时,旧的引用会被置为 null,新的引用会被重新赋值,这通常用于条件渲染或循环场景。
在 setup 中访问模板 ref
无论是 <script setup> 还是普通的 setup() 函数,你只能在组件挂载之后访问模板 ref。挂载前,ref.value 为 null。最常用的方式是在 onMounted 钩子内执行依赖DOM的操作。
<script setup>
import { ref, onMounted } from 'vue'
const divRef = ref(null)
onMounted(() => {
console.log(divRef.value) // <div>...</div>
})
// 错误:此时为 null
console.log(divRef.value)
</script>
<template>
<div ref="divRef">内容</div>
</template>
如果需要在更新后响应式地操作DOM,可以结合 watchEffect 或 nextTick,但这些仍然要求元素已经存在。例如,在 v-if 条件变更后,你可以使用 nextTick 等待DOM更新。
<script setup>
import { ref, nextTick, watchEffect } from 'vue'
const show = ref(false)
const box = ref(null)
watchEffect(async () => {
if (show.value) {
await nextTick()
box.value?.scrollIntoView()
}
})
</script>
使用 TypeScript 标注模板 ref 的类型
为模板 ref 添加类型标注可以获得更好的编辑器提示和编译时检查。根据引用目标的不同,类型声明略有差别。
引用原生DOM元素
直接使用泛型指定对应的DOM元素类型:
<script setup lang="ts">
import { ref } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)
</script>
<template>
<input ref="inputRef" />
</template>
Vue 会自动将解析到的DOM元素赋值进去,泛型保证了 .value 的类型为 HTMLInputElement | null。
引用子组件实例
如果引用的是Vue组件,需要导入组件的实例类型。Vue 3 提供了 ComponentPublicInstance 类型,或者直接从组件中导出类型。
<script setup lang="ts">
import { ref } from 'vue'
import MyModal from './MyModal.vue'
// 如果子组件使用了 defineExpose,则暴露的属性和方法在此类型上可用
const modalRef = ref<InstanceType<typeof MyModal> | null>(null)
onMounted(() => {
modalRef.value?.open() // open 方法由 defineExpose 暴露
})
</script>
<template>
<MyModal ref="modalRef" />
</template>
注意:只有子组件通过 defineExpose 显式暴露的成员,才能在父组件的 ref 中访问。如果需要自定义类型,可以单独声明接口并强制转换。
动态 ref 绑定与条件判断
除了前面提到的 :ref 动态名称,还可以通过计算属性或函数动态决定 ref 的绑定。但更常见的是在 v-for 中处理多个 ref。
在 v-for 中使用 ref
当在 v-for 循环中使用 ref 时,一个 ref 变量会被填充为一个包含所有对应DOM节点或组件实例的数组。
<script setup>
import { ref, onMounted } from 'vue'
const listRef = ref([]) // 注意:初始为空数组
onMounted(() => {
// listRef.value 是一个数组,包含所有 <li> 元素
listRef.value.forEach(el => console.log(el))
})
</script>
<template>
<ul>
<li v-for="item in 5" :key="item" ref="listRef">{{ item }}</li>
</ul>
</template>
这种方式获取到的数组顺序与渲染顺序一致。如果数据项变化,数组也会自动更新(新增或移除对应的DOM引用)。
动态 ref 数组的 TypeScript 类型
<script setup lang="ts">
import { ref } from 'vue'
const listRef = ref<HTMLElement[]>([])
</script>
注意事项与最佳实践
- 命名一致性:模板中的静态
ref="foo"必须与<script>中声明的const foo = ref(null)名称完全一致。这是Vue编译器通过编译时分析实现的自动绑定。 - 只读的 ref:你不能手动修改模板 ref 的
.value来替换DOM元素。它的值由Vue内部管理,直接修改可能导致不一致。 - 初始值为 null:始终为模板 ref 初始化为
null,因为元素在挂载前不存在。 - 访问时机:如需在
setup中同步获取元素引用(不推荐),可以使用nextTick或watch结合v-if,但最佳实践是使用onMounted或事件处理函数。 - 子组件暴露:如果希望父组件通过模板 ref 访问子组件的方法或属性,请确保子组件使用
defineExpose明确暴露,例如:// 子组件 defineExpose({ open, count })