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

来源:https://www.cnblogs.com/heavenYJJ/p/18395547
-Advertisement-
Play Games

前言 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。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • title: 使用 nuxi prepare 命令準備 Nuxt 項目 date: 2024/9/7 updated: 2024/9/7 author: cmdragon excerpt: 摘要:本文介紹nuxi prepare命令在Nuxt.js項目中的使用,該命令用於創建.nuxt目錄並生成類型 ...
  • 受 LabelImg 啟發的基於 web 的圖像標註工具,基於 Vue 框架 喲,網友們好,年更鴿子終於想起了他的博客園密碼。如標題所述,今天給大家帶來的是一個基於 vue2 的圖像標註工具。至於它誕生的契機呢,應該是我導 pass 掉了我的提議(讓甲方使用 LabelImg 進行數據標註),說是要 ...
  • title: 使用 nuxi init 創建全新 Nuxt 項目 date: 2024/9/6 updated: 2024/9/6 author: cmdragon excerpt: 摘要:本文介紹瞭如何使用nuxi init命令創建全新的Nuxt.js項目,包括安裝所需環境、命令使用方法、指定模板 ...
  • element-ui 的組件庫中沒有圖片下拉選擇組件,基於 el-select 組件做的改動並不能完全滿足需求,因此決定重寫一個。 從頭到尾做下來收穫很多,我決定把實現過程中遇到的問題記錄一下。 效果圖 線上試用地址 設計要點 接下來將上面代碼中的關鍵部分拆分介紹 1. 回顯選中的圖片和 label ...
  • Vue3.5正式版在這兩天發佈了,網上已經有了不少關於Vue3.5版本的解讀文章。但是歐陽發現這些文章對3.5中新增的功能介紹都不是很全,所以導致不少同學有個錯覺,覺得Vue3.5版本不過如此 ...
  • title: 使用 nuxi info 查看 Nuxt 項目詳細信息 date: 2024/9/5 updated: 2024/9/5 author: cmdragon excerpt: 摘要:文章介紹了nuxi info命令的使用方法,這是一個Nuxt.js命令行工具,用於查看當前或指定Nuxt項 ...
  • 第一步:安裝 npm 使用以下命令安裝npm install vue-grid-layout --save yarn 使用以下命令安裝yarn add vue-grid-layout 第二步:配置全局變數 import { createApp } from 'vue' import App from ...
  • 前言 前端導出表格有很多種方案,但是表格樣式一旦複雜了,那麼就得用代碼寫excel的樣式,還是比較麻煩的。每次樣式不一樣,就得重新寫,這時使用表格模板的優勢就體現出來了,想導出不同樣式的表格直接修改表格模板即可。 方案 我找了兩種方案: 1、使用xlsx-template,利用模板語法在xlsx中占 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...