Vue3 模板引用——ref
Vue3 模板引用——ref
- 1. 访问模板引用
- 1.1 在 onMounted 生命周期中使用(建议)
- 1.2 在 onMounted 生命周期前使用(不建议)
- 2. 组件上的 ref
- 2.1 父组件调用子组件方法和属性(子组件通过defineExpose暴露方法和属性)
- 2.2 子组件调用父组件的属性和方法(拓展,和 ref 没什么关系)
- 2.2.1 使用 props 和 emit(比较常用,父子通信)
- 2.2.2 使用 provide 和 inject(比较少用,跨级别父子通信)
- 2.2.2.1 使用方式
- 2.2.2.2 使用细节(调用最近父组件的变量和方法)
- 3. v-for 中的 ref
- 4. 函数模板引用
1. 访问模板引用
1.1 在 onMounted 生命周期中使用(建议)
一个特殊的attribute—— ref ,允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用:
<input ref="input">
让我们来看一个实例:
<template><div><input id="my-input" ref="input"></div>
</template><script setup>
import { useTemplateRef, onMounted } from 'vue'// 第一个参数必须与模板中的 ref 值匹配
const input = useTemplateRef('input')onMounted(() => {console.log('input:', input)console.log('input.value:', input.value)input.value.focus()
})
</script><style lang="scss" scoped></style>

可以看到模板引用,获取到的实际上是一个 DOM 元素 的 proxy 对象,其 .value 才是对应真正的 DOM 元素。
1.2 在 onMounted 生命周期前使用(不建议)
我们都知道,只有在组件挂载后才能访问模板引用。因此,如果在之前就进行引用访问,就会报错。比如:
<template><div><input id="my-input" ref="input"></div>
</template><script setup>
import { useTemplateRef, onMounted, watchEffect } from 'vue'// 第一个参数必须与模板中的 ref 值匹配
const input = useTemplateRef('input')watchEffect(() => {console.log('input:', input)console.log('input.value:', input.value)input.value.focus()
})
</script><style lang="scss" scoped></style>

可以在进行无值处理,或者将其放进 onMounted 生命周期中。比如:
<template><div><input id="my-input" ref="input"></div>
</template><script setup>
import { useTemplateRef, onMounted, watchEffect } from 'vue'// 第一个参数必须与模板中的 ref 值匹配
const input = useTemplateRef('input')watchEffect(() => {if(input.value) {console.log('input:', input)console.log('input.value:', input.value)input.value.focus()} else {// 此时还未挂载,或此元素已经被卸载(例如通过 v-if 控制),可以做一些提示类输出或操作}
})</script><style lang="scss" scoped></style>
2. 组件上的 ref
2.1 父组件调用子组件方法和属性(子组件通过defineExpose暴露方法和属性)
在 Vue2 中 ,我们可以通过 ref 直接获取到子组件定义的变量和方法。
但是在 Vue3 的组合式API中,因为我们使用了 <script setup>,导致子组件的任何东西都无法被访问,除非子组件在其中通过 defineExpose 宏显式暴露。下面是一个实例。
子组件Child.vue:
<template><div>{{ number }}</div>
</template><script setup>
import { ref } from 'vue'
const number = ref(1)
function add() {number.value++
}
// 暴露给父组件使用
defineExpose({number,add
})
</script><style lang="scss" scoped></style>
父组件App.vue:
<template><Child ref="child" /><button @click="addChild">+1</button>
</template><script setup>
import { useTemplateRef, onMounted } from 'vue'
import Child from '@/components/Child.vue'const childRef = useTemplateRef('child')onMounted(() => {// childRef.value 将持有 <Child /> 的实例console.log(childRef)console.log(childRef.value)
})function addChild() {childRef.value.add()
}
</script><style lang="scss" scoped></style>

2.2 子组件调用父组件的属性和方法(拓展,和 ref 没什么关系)
2.2.1 使用 props 和 emit(比较常用,父子通信)
子组件 Child.vue:
<template><div>{{ parentNumber }}</div><div>{{ hello }}</div><button @click="sayHello">调用父组件的方法</button>
</template><script setup>
import { ref, defineProps } from 'vue'
const hello = ref('Hello world!')
// 使用父组件变量
const props = defineProps({parentNumber: Number
})// 定义事件传递函数
const emit = defineEmits(["sayHello"])const sayHello = () => {// 向父组件传递事件和数据emit('sayHello', hello.value)
}
</script><style lang="scss" scoped></style>
父组件App.vue:
<template><Child ref="child" :parentNumber="number" @sayHello="handleSayHello" />
</template><script setup>
import { ref, useTemplateRef, onMounted } from 'vue'
import Child from '@/components/Child.vue'const number = ref(0)
const childRef = useTemplateRef('child')onMounted(() => {// childRef.value 将持有 <Child /> 的实例console.log(childRef)console.log(childRef.value)
})// 子组件触发的事件,在这里进行二次处理
function handleSayHello(hello) {console.log(hello)
}
</script><style lang="scss" scoped></style>

2.2.2 使用 provide 和 inject(比较少用,跨级别父子通信)
任意级别的父组件通过 provide 向子组件提供变量和方法;
任意级别的子组件通过 inject 拿到父组件提供的变量和方法。
2.2.2.1 使用方式
父组件 App.vue:
<template><div class="parent"><div>这是父组件</div><div>{{ number }}</div><Child /></div>
</template><script setup>
import { ref, provide } from 'vue'
import Child from '@/components/Child.vue'const number = ref(10)
const parentMethod = () => {console.log('调用了父组件方法');number.value++
}// 定义可向任意层级子组件传递的变量和方法
provide('number', number)
provide('parentMethod', parentMethod)
</script><style lang="scss" scoped>
.parent {color: red;border: 1px solid red;padding: 10px;
}
</style>
子组件Child.vue:
<template><div class="child"><div>这是子组件</div><GrandChild /></div>
</template><script setup>
import GrandChild from './GrandChild.vue';
</script><style lang="scss" scoped>
.child {color: green;border: 1px solid green;padding: 10px;
}
</style>
第二级子组件(这里简称 “孙子组件”) grandChild.vue:
<template><div class="grandcCild"><div>这是孙子组件</div><div>{{ parentNumber }}</div><button @click="callParentMethod">调用祖先中最近的parentMethod方法</button></div>
</template><script setup>
import { inject } from 'vue';// 获取任意级别父组件的变量和方法
const parentNumber = inject('number')
const parentMethod = inject('parentMethod')const callParentMethod = () => {if(parentMethod) {parentMethod();} else {console.log('父组件中没有该方法');}
}
</script><style lang="scss" scoped>
.grandcCild {color: blue;border: 1px solid blue;padding: 10px;
}
</style>

2.2.2.2 使用细节(调用最近父组件的变量和方法)
值得注意的是,如果多个级别的父组件都使用provide提供了相同名称的变量或方法,子组件在调用 inject 获取时只会获取到最近的组件的变量或属性。
比如,将 2.2.2.1 中的子组件 Child.vue 进行修改,也添加 number 和 parentMethod:
<template><div class="child"><div>这是子组件</div><div>{{ number }}</div><GrandChild /></div>
</template><script setup>
import { ref, provide } from 'vue';
import GrandChild from './GrandChild.vue';const number = ref(20);
const parentMethod = () => {console.log('调用了子组件方法');number.value++
}// 定义可向任意层级子组件传递的变量和方法
provide('number', number)
provide('parentMethod', parentMethod)
</script><style lang="scss" scoped>
.child {color: green;border: 1px solid green;padding: 10px;
}
</style>

3. v-for 中的 ref
<template><ul><li v-for="item in list" ref="items">{{ item }}</li></ul>
</template><script setup>
import { ref, useTemplateRef, onMounted } from 'vue'const list = ref([1, 2, 3])const itemRefs = useTemplateRef('items')onMounted(() => {console.log(itemRefs.value)console.log(itemRefs.value.map(i => i.textContent))
})
</script>

可以看到,v-for 中的 ref 的 .value 获取到的是所有 DOM 元素组成的数组。
4. 函数模板引用
在 3 的例子中,虽然能够取得整体的ref,但是无法针对某个具体的ref进行操作。Vue3 提供了函数模板引用,允许我们动态地使用函数设置el。
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
让我们来看下面一个例子。

假设我们有一大批水果,可以动态修改价格,用户希望在每个价格右侧添加一个编辑按钮,点击时可以聚焦到对应的价格输入框中。此时就可以使用函数动态设置ref,将 ref 存储在一个map或者set中,方便进行操作。
具体示例代码如下:
<template><div><div v-for="(fruit, index) in fruits" :key="index"><span>{{ fruit.name }}:</span><input type="number" v-model="fruit.price" placeholder="请输入水果价格" :ref="(el) => setInputRef(el, index)"/><button @click="handleEdit(index)">编辑</button></div></div>
</template><script setup>
import { ref, onMounted } from 'vue'const fruits = ref([{ id: 1, name: '苹果', price: 6 },{ id: 2, name: '香蕉', price: 3 },{ id: 3, name: '桃子', price: 4 },{ id: 4, name: '李子', price: 8 },
])
const inputRefMap = ref({})// 动态设置ref,并存储到map中
const setInputRef = (el, index) => {if(el) {inputRefMap.value[`input_ref_${index}`] = el}
}// 取得对应的input元素并聚焦
const handleEdit = (index) => {inputRefMap.value[`input_ref_${index}`].focus()
}onMounted(() => {
})
</script><style lang="scss" scoped>
input{margin-right: 10px;
}
</style>
上一章 《Vue3 基础实战练习》
下一章 《Vue3 生命周期》
