在 Vue 3 中,ref
是一個核心的響應式 API,但它在模板中還有另一個非常重要的用途:獲取對 DOM 元素或子組件實例的直接引用。這就是我們所說的“模板引用”。
核心概念
- 目的:讓你在父組件中能夠直接訪問并操作特定的 DOM 元素或子組件實例。
- 場景:
- 手動聚焦一個輸入框。
- 觸發一個 DOM 元素上的動畫。
- 調用子組件暴露的特定方法(非響應式數據)。
- 測量 DOM 元素的尺寸。
- 與需要直接 DOM 訪問的第三方庫集成。
基本用法
在模板中聲明
ref
在你想要引用的元素或組件上,使用
ref
attribute(在 Vue 3 的<script setup>
中,這實際上是一個特殊的指令,但用法像 attribute)。<template><!-- 引用一個 DOM 元素 --><input ref="inputRef" type="text" placeholder="請輸入..." /><!-- 引用一個子組件 --><ChildComponent ref="childRef" /> </template>
在
<script setup>
中定義響應式引用使用
ref
函數在<script setup>
中聲明一個變量,這個變量的名字必須與模板中ref
attribute 的值完全一致。Vue 會自動將 DOM 元素或組件實例賦值給這個響應式引用。<script setup> import { ref, onMounted } from 'vue' import ChildComponent from './ChildComponent.vue'// 定義響應式引用,名字必須與模板中的 ref 值匹配 const inputRef = ref(null) const childRef = ref(null)// 組件掛載后,引用才可用 onMounted(() => {// 訪問 DOM 元素if (inputRef.value) {inputRef.value.focus() // 讓輸入框自動獲得焦點console.log('Input width:', inputRef.value.offsetWidth)}// 調用子組件的方法 (假設子組件暴露了 doSomething 方法)if (childRef.value) {childRef.value.doSomething()} }) </script>
關鍵要點與注意事項
響應式引用 (
ref
) vs 模板引用 (ref
attribute):ref()
?是一個函數,用于創建一個響應式引用對象。這個對象有一個?.value
?屬性,用來存儲值(在這里是 DOM 元素或組件實例)。- 模板中的?
ref="xxx"
?是一個特殊的 attribute,它告訴 Vue 將這個元素/組件的引用注入到名字為?xxx
?的響應式引用 (ref
) 中。 - 名字必須匹配:
const xxx = ref(null)
?和?ref="xxx"
?中的?xxx
?必須完全相同。
初始值與訪問時機:
- 通常將響應式引用初始化為?
null
?(const myRef = ref(null)
),因為在組件掛載前,DOM 元素或子組件實例還不存在。 - 在?
onMounted
?生命周期鉤子之前,引用的?.value
?通常是?null
。因為 DOM 渲染發生在?onMounted
?之后。 - 最佳實踐:在?
onMounted
?或?onUpdated
?鉤子中訪問引用,或者在事件處理函數中(確保元素已渲染)。
- 通常將響應式引用初始化為?
引用類型:
- DOM 元素:引用?
.value
?直接指向原生的 DOM 元素對象(如?HTMLInputElement
,?HTMLDivElement
?等),你可以調用其所有原生方法和屬性。 - 子組件:引用?
.value
?指向子組件的實例。你可以訪問子組件的公開屬性和方法(即在?setup
?返回或在?<script setup>
?中用?defineExpose
?暴露的屬性/方法)。
<!-- ChildComponent.vue --> <script setup> import { ref } from 'vue'const count = ref(0)// 暴露給父組件的方法 function increment() {count.value++ }// 明確暴露哪些屬性/方法給父組件 defineExpose({increment,// count // 也可以暴露響應式數據,但需謹慎 }) </script>
- DOM 元素:引用?
訪問子組件的
$el
:- 在 Vue 3 的 Composition API 中,子組件實例本身不直接是 DOM 元素。如果你需要訪問子組件的根 DOM 元素,可以通過子組件實例的?
$.vnode.el
?屬性(這是 Vue 內部的,不推薦直接依賴)或者讓子組件通過?defineExpose
?暴露其根元素的引用。
- 在 Vue 3 的 Composition API 中,子組件實例本身不直接是 DOM 元素。如果你需要訪問子組件的根 DOM 元素,可以通過子組件實例的?
v-for
中的模板引用:- 當?
ref
?用在?v-for
?內部的元素或組件上時,對應的引用將是一個包含相應數據的數組,順序與?v-for
?渲染的順序一致。 - 重要:
ref
?不會隨著?v-for
?數據的更新而自動同步更新數組。如果數據列表變化(增刪改),你需要手動管理這個引用數組,或者考慮使用其他模式(如?key
?+ 計算屬性)。
<template><div v-for="(item, index) in list" :key="item.id" :ref="el => divs[index] = el">{{ item.text }}</div> </template><script setup> import { ref, reactive, onBeforeUpdate } from 'vue'const list = ref([{ id: 1, text: 'A' }, { id: 2, text: 'B' }])// 使用函數式 ref 回調來更靈活地收集引用 const divs = ref([])// 在每次更新前重置引用數組,避免殘留 onBeforeUpdate(() => {divs.value = [] }) </script>
- 當?
函數式
ref
:- 除了字符串?
ref="xxx"
,你還可以傳遞一個函數?:ref="callback"
。這個函數會在每次組件更新時被調用,接收 DOM 元素或組件實例作為參數。這在需要更精細控制引用收集邏輯時非常有用(如上面?v-for
?的例子)。
<template><input :ref="(el) => inputElement = el" /> </template><script setup> import { ref } from 'vue'const inputElement = ref(null) // 函數內部會設置它// 或者更復雜的邏輯 const setupInputRef = (el) => {if (el) {// 元素被掛載inputElement.value = el// 可以在這里做初始化操作} else {// 元素被卸載inputElement.value = null} } </script>
- 除了字符串?
TypeScript 支持:
- 在 TypeScript 中,你可以為模板引用提供精確的類型。
<script setup lang="ts"> import { ref, onMounted } from 'vue' import ChildComponent from './ChildComponent.vue'// 為 DOM 元素引用提供類型 const inputRef = ref<HTMLInputElement | null>(null)// 為子組件引用提供類型 (需要導入組件類型) const childRef = ref<InstanceType<typeof ChildComponent> | null>(null)onMounted(() => {if (inputRef.value) {inputRef.value.focus() // TypeScript 知道這是 HTMLInputElement}if (childRef.value) {childRef.value.increment() // TypeScript 知道可以調用 increment} }) </script>
Vue 3 的模板引用 (ref
) 是一個強大且常用的特性,用于在父組件中直接操作 DOM 或子組件。核心是:
- 在模板中使用?
ref="myName"
。 - 在?
<script setup>
?中使用?const myName = ref(null)
?定義響應式引用。 - 在?
onMounted
?或之后訪問?myName.value
?來獲取 DOM 元素或組件實例。 - 對于子組件,使用?
defineExpose
?來控制暴露的 API。 - 在?
v-for
?中使用時,引用是數組,需注意更新時機。 - 在 TypeScript 中提供精確類型。
原則:盡量通過響應式數據和 props/events 來進行組件通信,僅在確實需要直接 DOM 操作或調用特定方法時才使用模板引用。