牛逼!Vue3.5的useTemplateRef讓ref操作DOM更加絲滑

前端欧阳發表於2024-09-04

前言

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。

相關文章