前言
vue3中想要訪問DOM和子元件可以使用ref進行模版引用,但是這個ref有一些讓人迷惑的地方。比如定義的ref變數到底是一個響應式資料還是DOM元素?還有template中ref屬性的值明明是一個字串,比如ref="inputEl"
,怎麼就和script中同名的inputEl
變數綁到一塊了呢?所以Vue3.5推出了一個useTemplateRef
函式,完美的解決了這些問題。
關注公眾號:【前端歐陽】,給自己一個進階vue的機會
ref模版引用的問題
我們先來看一個react
中使用ref訪問DOM元素的例子,程式碼如下:
const inputEl = useRef<HTMLInputElement>(null);
<input type="text" ref={inputEl} />
使用useRef
函式定義了一個名為inputEl
的變數,然後將input元素的ref屬性值設定為inputEl
變數,這樣就可以透過inputEl
變數訪問到input輸入框了。
inputEl
因為是一個.current
屬性的物件,由於inputEl
變數賦值給了ref屬性,所以他的.current
屬性的值被更新為了input DOM元素,這個做法很符合程式設計直覺。
再來看看vue3
中的做法,相比之下就很不符合程式設計直覺了。
不知道有多少同學和歐陽一樣,最開始接觸vue3時總是在template中像react
一樣給ref屬性繫結一個ref變數,而不是ref變數的名稱。比如下面這樣的程式碼:
<input type="text" :ref="inputEl" />
const inputEl = ref<HTMLInputElement>();
更加要命的是這樣寫還不會報錯!!!!當我們使用inputEl
變數去訪問input輸入框時始終拿到的都是undefined
。
經過多次排查發現原來ref屬性接收的不是一個ref變數,而是ref變數的名稱。正確的程式碼應該是這樣的:
<input type="text" ref="inputEl" />
const inputEl = ref<HTMLInputElement>();
還有就是如果我們將ref模版引用相關的邏輯抽成hooks後,那麼必須將在vue元件中也要將ref屬性對應的ref變數也定義才可以。
hooks程式碼如下:
export default function useRef() {
const inputEl = ref<HTMLInputElement>();
function setInputValue() {
if (inputEl.value) {
inputEl.value.value = "Hello, world!";
}
}
return {
inputEl,
setInputValue,
};
}
在hooks中定義了一個名為inputRef
的變數,並且在setInputValue
函式中會透過inputRef
變數對input輸入框進行操作。
vue元件程式碼如下:
<template>
<input type="text" ref="inputEl" />
<button @click="setInputValue">給input賦值</button>
</template>
<script setup lang="ts">
import useInput from "./useInput";
const { setInputValue, inputEl } = useInput();
</script>
雖然在vue元件中我們不會使用inputEl
變數,但是還是需要從hooks中匯入useInput
變數。大家不覺得這很奇怪嗎?匯入了一個變數,又沒有顯式的去使用這個變數。
如果在這裡不去從hooks中匯入inputEl
變數,那麼inputEl
變數中就不能繫結上input輸入框了。
useTemplateRef函式
為了解決上面說的ref模版引用的問題,在Vue3.5中新增了一個useTemplateRef
函式。
useTemplateRef
函式的用法很簡單:只接收一個引數key
,是一個字串。返回值是一個ref變數。
其中引數key字串的值應該等於template中ref屬性的值。
返回值是一個ref變數,變數的值指向模版引用的DOM元素或者子元件。
我們來看個例子,前面的demo改成useTemplateRef
函式後程式碼如下:
<template>
<input type="text" ref="inputRef" />
<button @click="setInputValue">給input賦值</button>
</template>
<script setup lang="ts">
import { useTemplateRef } from "vue";
const inputEl = useTemplateRef<HTMLInputElement>("inputRef");
function setInputValue() {
if (inputEl.value) {
inputEl.value.value = "Hello, world!";
}
}
</script>
在template中ref屬性的值為字串"inputRef"
。
在script中使用useTemplateRef
函式,傳入的第一個引數也是字串"inputRef"
。useTemplateRef
函式的返回值就是指向input輸入框的ref變數。
由於inputEl
是一個ref變數,所以在click事件中想要訪問到DOM元素input輸入框就需要使用inputEl.value
。
我們這裡是要給輸入框中塞一個字串"Hello, world!",所以使用inputEl.value.value = "Hello, world!"
使用了useTemplateRef
函式後和之前比起來就很符合程式設計直覺了。template中ref屬性值是一個字串"inputRef"
,使用useTemplateRef
函式時也傳入字串"inputRef"
就能拿到對應的模版引用了。
hooks中使用useTemplateRef
回到前面講的hooks的例子,使用useTemplateRef
後hooks程式碼如下:
export default function useInput(key) {
const inputEl = useTemplateRef<HTMLInputElement>(key);
function setInputValue() {
if (inputEl.value) {
inputEl.value.value = "Hello, world!";
}
}
return {
setInputValue,
};
}
現在我們在hooks中就不需要匯出變數inputEl
了,因為這個變數只需要在hooks內部使用。
vue元件程式碼如下:
<template>
<input type="text" ref="inputRef" />
<button @click="setInputValue">給input賦值</button>
</template>
<script setup lang="ts">
import useInput from "./useInput";
const { setInputValue } = useInput("inputRef");
</script>
由於在vue元件中我們不需要使用inputEl
變數,所以在這裡就不需要從useInput
中引入變數inputEl
了。而之前不使用useTemplateRef
的方案中我們就不得不引入inputEl
變數了。
動態切換ref繫結的變數
有的時候我們需要根據不同的場景去動態切換ref模版引用的變數,這時在template中ref屬性的值就是動態的了,而不是一個寫死的字串。在這種場景中useTemplateRef
也是支援的,程式碼如下:
<template>
<input type="text" :ref="refKey" />
<button @click="switchRef">切換ref繫結的變數</button>
<button @click="setInputValue">給input賦值</button>
</template>
<script setup lang="ts">
import { useTemplateRef, ref } from "vue";
const refKey = ref("inputEl1");
const inputEl1 = useTemplateRef<HTMLInputElement>("inputEl1");
const inputEl2 = useTemplateRef<HTMLInputElement>("inputEl2");
function switchRef() {
refKey.value = refKey.value === "inputEl1" ? "inputEl2" : "inputEl1";
}
function setInputValue() {
const curEl = refKey.value === "inputEl1" ? inputEl1 : inputEl2;
if (curEl.value) {
curEl.value.value = "Hello, world!";
}
}
</script>
在這個場景template中ref繫結的就是一個變數refKey
,透過點選切換ref繫結的變數
按鈕可以切換refKey
的值。相應的,繫結input輸入框的變數也會從inputEl1
變數切換成inputEl2
變數。
useTemplateRef
是如何實現的?
我們來看看useTemplateRef
的原始碼,其實很簡單,簡化後的程式碼如下:
function useTemplateRef(key) {
const i = getCurrentInstance();
const r = shallowRef(null);
if (i) {
const refs = i.refs === EMPTY_OBJ ? (i.refs = {}) : i.refs;
Object.defineProperty(refs, key, {
enumerable: true,
get: () => r.value,
set: (val) => (r.value = val),
});
}
return r;
}
首先使用getCurrentInstance
方法獲取當前vue例項物件,賦值給變數i
。
然後呼叫shallowRef
函式生成一個淺層的ref物件,初始值為null。這個ref物件就是useTemplateRef
函式返回的ref物件。
接著就是判斷當前vue例項如果存在就讀取例項上面的refs
屬性物件,如果例項物件上面沒有refs
屬性,那麼就初始化一個空物件到vue例項物件的refs
屬性。
vue例項物件上面的這個refs
屬性物件用過vue2的同學應該都很熟悉,裡面存的是註冊過ref屬性的所有 DOM 元素和元件例項。
vue3雖然不像vue2一樣將refs
屬性物件開放給開發者,但是他的內部依然還是用vue例項上面的refs
屬性物件來儲存template中使用ref屬性註冊過的元素和元件例項。
這裡使用了Object.defineProperty
方法對refs
屬性物件進行攔截,攔截的欄位是變數key
的值,而這個key
的值就是template中使用ref屬性繫結的值。
以我們上面的demo舉例,在template中的程式碼如下:
<input type="text" ref="inputRef" />
這裡使用ref屬性在vue例項的refs
屬性物件上面註冊了一個input輸入框,refs.inputRef
的值就是指向DOM元素input輸入框。
然後在script中是這樣使用useTemplateRef
的:
const inputEl = useTemplateRef<HTMLInputElement>("inputRef")
呼叫useTemplateRef
函式時傳入的是字串"inputRef"
,在useTemplateRef
函式內部使用Object.defineProperty
方法對refs
屬性物件進行攔截,攔截的欄位為變數key
的值,也就是呼叫useTemplateRef
函式傳入的字串"inputRef"
。
初始化時,vue處理input輸入框上面的ref="inputRef"
就會執行下面這樣的程式碼:
refs[ref] = value
此時的value
的值就是指向DOM元素input輸入框,ref
的值就是字串"inputRef"
。
那麼這行程式碼就是將DOM元素input輸入框賦值給refs
物件上面的inputRef
屬性上。
由於這裡對refs
物件上面的inputRef
屬性進行寫操作,所以會走到useTemplateRef
函式中Object.defineProperty
定義的set
攔截。程式碼如下:
const r = shallowRef(null);
Object.defineProperty(refs, key, {
enumerable: true,
get: () => r.value,
set: (val) => (r.value = val),
});
在set
攔截中會將DOM元素input輸入框賦值給ref變數r
,而這個r
就是useTemplateRef
函式返回的ref變數。
同樣的當物件refs
物件的inputRef
屬性進行讀操作時,也會走到這裡的get
攔截中,返回useTemplateRef
函式中定義的ref變數r
的值。
總結
Vue3.5中新增的useTemplateRef
函式解決了ref屬性中存在的幾個問題:
-
不符合程式設計直覺,template中ref屬性的值是script中對應的ref變數的變數名。
-
在script中如果不使用ts,則不能直觀的知道一個ref變數到底是響應式資料還是DOM元素?
-
將定義和訪問DOM元素相關的邏輯抽到hooks中後,雖然vue元件中不會使用到存放DOM元素的變數,但是也必須在元件中從hooks中匯入。
接著我們講了useTemplateRef
函式的實現。在useTemplateRef
函式中會定義一個ref物件,在useTemplateRef
函式最後就是return返回這個ref物件。
接著使用Object.defineProperty
對vue例項上面的refs
屬性物件進行get和set攔截。
初始化時,處理template中的ref屬性,會對vue例項上面的refs
屬性物件進行寫操作。
然後就會被set攔截,在set攔截中會將useTemplateRef
函式中定義的ref物件的值賦值為繫結的DOM元素或者元件例項。
而useTemplateRef
函式就是將這個ref物件進行return返回,所以我們可以透過useTemplateRef
函式的返回值拿到template中ref屬性繫結的DOM元素或者元件例項。
關注公眾號:【前端歐陽】,給自己一個進階vue的機會
另外歐陽寫了一本開源電子書vue3編譯原理揭秘,看完這本書可以讓你對vue編譯的認知有質的提升。這本書初、中級前端能看懂,完全免費,只求一個star。