前言 vue3中想要訪問DOM和子組件可以使用ref進行模版引用,但是這個ref有一些讓人迷惑的地方。比如定義的ref變數到底是一個響應式數據還是DOM元素?還有template中ref屬性的值明明是一個字元串,比如ref="inputEl",怎麼就和script中同名的inputEl變數綁到一塊了 ...
前言
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。