Template Refs 模板引用

文章目录

  • 前言
  • [一、获取 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 暴露子组件 API
  • v-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 注意生命周期

第三方库通常需要:

  1. onMounted 中初始化(ref 已有 DOM)
  2. onUnmounted 中销毁,避免内存泄漏
  3. 数据变化时在 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)。


八、易混淆点

  1. ref 名称必须匹配 :模板 ref="xxx" 与 script const xxx = ref(null) 同名。
  2. 挂载前为 null:不要用 onBeforeMount 里操作 DOM ref。
  3. script setup 子组件需 defineExpose:否则 formRef.value.validate 为 undefined。
  4. v-for 的 ref 用函数或数组:单个 ref 变量只会指向最后一个元素。
  5. 组件 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 Refref="name" + 同名 ref(null) 获取 DOM 或组件实例
  • 挂载前 null:onMounted / nextTick 后再访问
  • defineExpose:script setup 子组件显式暴露给父组件 ref 调用
  • v-for:函数 ref 或 ref 数组收集多个元素
  • TSHTMLInputElementInstanceType<typeof Comp> 标注类型