文章目录
- 前言
- [一、获取 DOM 元素](#一、获取 DOM 元素)
-
- [1.1 基本用法](#1.1 基本用法)
- [1.2 挂载前 ref 为 null](#1.2 挂载前 ref 为 null)
- [1.3 常见 DOM 操作](#1.3 常见 DOM 操作)
- 二、获取子组件实例
-
- [2.1 调用子组件方法](#2.1 调用子组件方法)
- [2.2 defineExpose 的作用](#2.2 defineExpose 的作用)
- [三、v-for 中的 ref](#三、v-for 中的 ref)
-
- [3.1 函数形式收集](#3.1 函数形式收集)
- [3.2 数组形式(Vue 3.5+)](#3.2 数组形式(Vue 3.5+))
- 四、第三方库初始化
-
- [4.1 ECharts 示例](#4.1 ECharts 示例)
- [4.2 注意生命周期](#4.2 注意生命周期)
- [五、TypeScript 类型](#五、TypeScript 类型)
-
- [5.1 DOM 元素类型](#5.1 DOM 元素类型)
- [5.2 组件实例类型](#5.2 组件实例类型)
- [六、函数 ref](#六、函数 ref)
-
- [6.1 动态绑定](#6.1 动态绑定)
- [6.2 与字符串 ref 对比](#6.2 与字符串 ref 对比)
- 七、面试聚焦
-
- [7.1 挂载前 ref 为 null](#7.1 挂载前 ref 为 null)
- [7.2 script setup 子组件默认暴露什么?](#7.2 script setup 子组件默认暴露什么?)
- [7.3 为什么需要 defineExpose?](#7.3 为什么需要 defineExpose?)
- 八、易混淆点
- 九、思考与练习
- 总结
前言
Template Refs(模板引用)让你在 Composition API 中获取模板里的 DOM 元素或子组件实例,便于调用原生方法、第三方库初始化或父调子方法。本篇会讲清楚:
- ref 获取 DOM 与组件实例
defineExpose暴露子组件 APIv-for中的 ref 收集- TypeScript 类型与常见陷阱
一、获取 DOM 元素
1.1 基本用法
vue
<template>
<input ref="inputRef" placeholder="自动聚焦" />
<button @click="focusInput">聚焦</button>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
inputRef.value?.focus()
})
const focusInput = () => {
inputRef.value?.focus()
}
</script>
规则 :模板中 ref="inputRef" 与 script 中同名 ref(null) 自动绑定。
1.2 挂载前 ref 为 null
javascript
const inputRef = ref(null)
console.log(inputRef.value) // null(setup 同步阶段)
onMounted(() => {
console.log(inputRef.value) // <input> DOM 元素
})
ref 在组件挂载完成后 才有值。setup 同步执行期间、onMounted 之前访问都是 null,需用可选链 ?. 或放在 onMounted / nextTick 中。
1.3 常见 DOM 操作
vue
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
const containerRef = ref(null)
onMounted(() => {
// 输入框
inputRef.value?.focus()
inputRef.value?.select()
// 滚动
containerRef.value?.scrollIntoView({ behavior: 'smooth' })
// 尺寸
const { width, height } = containerRef.value.getBoundingClientRect()
})
</script>
<template>
<input ref="inputRef" />
<div ref="containerRef">内容区域</div>
</template>
二、获取子组件实例
2.1 调用子组件方法
vue
<!-- ChildForm.vue -->
<script setup>
import { ref } from 'vue'
const formData = ref({ name: '' })
const validate = () => {
if (!formData.value.name) return false
return true
}
const reset = () => {
formData.value = { name: '' }
}
// script setup 默认不暴露,必须 defineExpose
defineExpose({ validate, reset })
</script>
<template>
<input v-model="formData.name" />
</template>
vue
<!-- Parent.vue -->
<template>
<ChildForm ref="formRef" />
<button @click="submit">提交</button>
</template>
<script setup>
import { ref } from 'vue'
import ChildForm from './ChildForm.vue'
const formRef = ref(null)
const submit = () => {
if (formRef.value?.validate()) {
// 提交逻辑
}
}
</script>
2.2 defineExpose 的作用
| 场景 | 行为 |
|---|---|
| Options API | 子组件 methods 默认可被父组件 ref 访问 |
| script setup | 默认什么都不暴露,父组件 ref 拿不到内部 |
| defineExpose | 显式指定父组件可访问的属性/方法 |
javascript
// 暴露部分
defineExpose({ validate, reset })
// 暴露全部(不推荐)
defineExpose({ ...instance })
三、v-for 中的 ref
3.1 函数形式收集
vue
<template>
<div
v-for="item in list"
:key="item.id"
:ref="(el) => setItemRef(el, item.id)"
>
{{ item.name }}
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([
{ id: 1, name: 'A' },
{ id: 2, name: 'B' }
])
const itemRefs = ref(new Map())
const setItemRef = (el, id) => {
if (el) {
itemRefs.value.set(id, el)
} else {
itemRefs.value.delete(id) // 卸载时 el 为 null
}
}
onMounted(() => {
console.log(itemRefs.value.get(1)) // 第一个 div
})
</script>
3.2 数组形式(Vue 3.5+)
vue
<template>
<div v-for="item in list" :key="item.id" ref="itemRefs">
{{ item.name }}
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const itemRefs = ref([])
onMounted(() => {
console.log(itemRefs.value) // [div, div, ...]
})
</script>
列表更新时 ref 数组会同步更新。函数形式更灵活,适合按 id 索引。
四、第三方库初始化
4.1 ECharts 示例
vue
<template>
<div ref="chartRef" style="width: 600px; height: 400px;"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
const chartRef = ref(null)
let chartInstance = null
onMounted(() => {
chartInstance = echarts.init(chartRef.value)
chartInstance.setOption({ /* ... */ })
})
onUnmounted(() => {
chartInstance?.dispose()
})
</script>
4.2 注意生命周期
第三方库通常需要:
- onMounted 中初始化(ref 已有 DOM)
- onUnmounted 中销毁,避免内存泄漏
- 数据变化时在 watch 中 update,而非重复 init
五、TypeScript 类型
5.1 DOM 元素类型
typescript
import { ref, onMounted } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)
const divRef = ref<HTMLDivElement | null>(null)
onMounted(() => {
inputRef.value?.focus() // 有类型提示
})
5.2 组件实例类型
typescript
import type { ComponentPublicInstance } from 'vue'
import ChildForm from './ChildForm.vue'
// 方式一:InstanceType
const formRef = ref<InstanceType<typeof ChildForm> | null>(null)
// 方式二:自定义暴露的类型
interface ChildFormExpose {
validate: () => boolean
reset: () => void
}
const formRef = ref<ChildFormExpose | null>(null)
配合 defineExpose 时,可单独定义 Expose 接口供父组件使用。
六、函数 ref
6.1 动态绑定
vue
<template>
<input :ref="(el) => { inputRef = el }" />
</template>
<script setup>
let inputRef = null // 也可配合 ref()
</script>
函数 ref 在元素挂载时传入 el,卸载时传入 null,适合条件渲染或 v-for。
6.2 与字符串 ref 对比
| 方式 | 写法 | 适用 |
|---|---|---|
| 同名 ref 变量 | ref="inputRef" + const inputRef = ref(null) |
单个元素,最常用 |
| 函数 ref | :ref="fn" |
v-for、动态、条件渲染 |
七、面试聚焦
7.1 挂载前 ref 为 null
setup 同步阶段和 onMounted 之前,ref.value 为 null。访问 DOM 或调用子组件方法必须在 onMounted、nextTick 或事件回调中。
7.2 script setup 子组件默认暴露什么?
什么都不暴露 。父组件通过 ref 无法访问子组件内部,必须用 defineExpose 显式暴露需要的方法或数据。
7.3 为什么需要 defineExpose?
script setup 将组件内部封闭,避免父组件随意访问实现细节,符合封装原则。只暴露必要的 API(如 validate、focus)。
八、易混淆点
- ref 名称必须匹配 :模板
ref="xxx"与 scriptconst xxx = ref(null)同名。 - 挂载前为 null:不要用 onBeforeMount 里操作 DOM ref。
- script setup 子组件需 defineExpose:否则 formRef.value.validate 为 undefined。
- v-for 的 ref 用函数或数组:单个 ref 变量只会指向最后一个元素。
- 组件 ref vs DOM ref:组件实例调用方法;DOM ref 操作 .focus、.scrollIntoView 等。
九、思考与练习
1. Template Ref 的作用是什么?
解析:获取模板中 DOM 元素或子组件实例的直接引用,用于原生 DOM 操作、调用子组件方法、第三方库初始化。
2. 为什么 onMounted 之前 ref 是 null?
解析:DOM 尚未渲染完成,ref 绑定发生在挂载过程中,挂载完成后才有值。
3. script setup 子组件如何让父组件调用 validate?
解析:子组件 defineExpose({ validate }),父组件 formRef.value?.validate()。
4. v-for 中如何收集多个元素 ref?
解析:用函数 ref :ref="(el) => setRef(el, id)" 存 Map,或 Vue 3.5+ 用 ref 数组。
5. 何时用 ref 操作 DOM,何时用声明式?
解析:focus、scroll、第三方库 init 等命令式操作用 ref;展示、样式、数据绑定优先声明式模板。
总结
- Template Ref :
ref="name"+ 同名ref(null)获取 DOM 或组件实例 - 挂载前 null:onMounted / nextTick 后再访问
- defineExpose:script setup 子组件显式暴露给父组件 ref 调用
- v-for:函数 ref 或 ref 数组收集多个元素
- TS :
HTMLInputElement、InstanceType<typeof Comp>标注类型