Vue3.2: Ref操作Dom為何既易用又高效?

糖墨夕發表於2023-01-13

緣起

在開發一個專案之前,我們往往都是先做下需求分析,針對前端而言,就是可以調研或者選擇一個基礎元件庫,來提高我們的工作效率。畢竟,對比較計較時間成本的公司來說,不會給你拿看電視劇玩遊戲的時間去專門開發一個類似日曆的元件。但在市面上的元件庫,並不能都能滿足我們的需求。這個時候,我們就需要自己手寫元件,來應用到專案中。

而這就是我想說的: 如何設計元件,讓其既能易於應用(或者說減少程式碼量),又能提高擴充套件性,方便需求變更和後續維護?

可以有很多種方式,而利用Ref操作Dom的特性隨是其中之一,但這個方式卻讓我們在維護和操作Modal、Popup以及頻繁操作Dom顯示和隱藏互動的元件的時候,卻發揮得很大優勢。

就對Ref操作Dom的相關知識點以及應用例項分幾個方面來做下剖析

  • Ref獲取Dom的本質
  • Ref操作Dom在Vue2.x和Vue3.x的不同
  • Ref操作元件Dom和父子元件單向傳遞對比

詳說

Ref獲取Dom的本質

Vue2.x中Vue的物件屬性$refs,其實就是所有註冊過的ref的一個集合,而ref對應著template模版中,不同元件或普通Dom元素上關聯的ref="xx"; 原始碼中ref的實際獲取方式也是透過原生方式getElementById而得到的Dom節點;
可以說ref是document.getElementById的語法糖。vue3的ref延續了vue2的用法,還增加了一個作用就是建立響應式資料

也許有人會問了,既然ref和getElementById都能獲取到Dom,那麼在專案開發中,我選擇哪種方式都沒什麼區別呢?

關於這個問題,經過資料表明,$refs相對document.getElementById的方法,會減少獲取dom節點的消耗;而具體原因,等下一篇文章再詳細探討。

Ref操作Dom在Vue2.x和Vue3.x的不同

Vue2.x

我們只需要在相應的Dom元素或者元件加上ref="xx"屬性,然後在Vue物件中使用this.$refs.xx,就可以直接獲取到該Dom並操作其方法屬性,

<user-and-dep-tree-select-modal
  ref="avaUserTreeSelect"
  title="選擇可見範圍"
  :project-id="currentProjectId"
  :visible.sync="avaUserModalVisible"
  @ok="editAvailableUser"
/>
或者
<div class="user" ref="user">dd</div>
// $refs
showManagerModal () {
  this.$refs.avaUserTreeSelect.showModal(this.form.managers)
  console.log(this.$refs.user.text)
},

Vue3.2

在Vue3.2版本使用的方式

//普通Dom
<div class="user" ref="user"></div>
//元件
<batch-adjust-department-modal ref="batchAdjustDepartmentRef" />
<script setup lang="ts">
import { ref } from 'vue';
// modal調整部門彈層Dom
const batchAdjustDepartmentRef = ref(null);
const user = ref(null);
</script>

也許這裡有人疑問,為什麼宣告瞭一個和template的ref中同名的常量變數就繫結了對應的dom?
在這裡再補充說明一下:

  • Vue3 在早期版本( 3.0.0-beta.21 之前)中對 composition api 的支援,只能在元件選項 setup 函式中使用。而相應式的變數都是透過在setup()方法中return {寫入需要在模版中使用的變數或方法}

    <script>
    import { defineComponent, ref } from 'vue'
    
    export default defineComponent({
      name: 'HelloWorld',
      setup(props, ctx) {
    const count = ref(0)
    function add() {
      count.value++
    }
    // 使用return {} 把變數、方法暴露給模板
    return {
      count,
      add,
    }
      },
    })
    </script>
  • 在 3.0.0-beta.21 版本中增加了 <script setup> 的實驗特性。如果使用了,會提示 <script setup> 還處在實驗特性階段。
  • 在 3.2.0 版本中移除 <script setup> 的實驗狀態,從此,宣告 <script setup> 正式轉正使用,成為框架穩定的特性之一
    與元件選項 setup 函式對比, <script setup> 我們只需要寫更少、更簡潔的程式碼,不需要使用 return {} 暴露變數和方法了,使用元件時不需要主動註冊了,會自動幫你繫結

所以在<script setup>中宣告的變數會自動被加到該Vue物件的本身this中,如

<script setup><script>
const user = ref(null);this.$ref.user

Ref操作元件Dom和父子元件單向傳遞

props父傳子,子透過emits傳父,這樣單方向傳遞,在控制彈層元件的顯示和隱藏方面也可以實現,但是如此一來,我們就會像下面一樣
父元件

<template>
  <exchange-valid-modal-vue v-model:visible="visibleExchange" />
</template>
<script setup lang="ts">
// 轉讓管理員元件
import ExchangeValidModalVue from './modal/ExchangeValidModal.vue';
// modal彈層
const visibleExchange = ref(false); // 轉讓管理員視覺化
const onExchangeAdmin = () => {
  visibleExchange.value = true;
};
</script>

子元件ExchangeValidModalVue.vue

<template>
    <t-dialog
      v-model:visible="visibleExchange"
      header="轉讓主管理員"
      attach="body"
      width="384px"
      :confirm-on-enter="true"
      :on-close="onCloseExchange"
    />
</template>
<script setup lang="ts">
const visibleExchange = ref(false);
const props = defineProps({
  data: {
    type: Object,
    default: () => {},
  },
  visible: {
    type: Boolean,
    default: false,
  },
});
watch(
  () => props.visible,
  (cur, pre) => {
    visibleExchange.value = cur;
    if (cur) {
      firstTag.value = 1;
    }
  },
);
watch(
  () => visibleExchange.value,
  (cur, pre) => {
    emits('update:visible', cur);
  },
);
</script>

從程式碼裡面我們就可以發現透過用父子元件單向傳遞的方式去實現一個元件的顯示和隱藏功能,我們需要如此費勁地宣告多個變數,還要做兩次監聽,萬一後面不止一個這樣的引數進行傳遞,那麼程式碼量可想而知,而且也不易維護。

其實顯示和隱藏的功能可以直接在內部中進行值的響應即可,並不需要在父級別中操作,如下將上面程式碼改變一下:

子元件ExchangeValidModalVue.vue

<template>
    <t-dialog
      v-model:visible="visible"
      header="轉讓主管理員"
      attach="body"
      width="384px"
      :confirm-on-enter="true"
      :on-close="onCloseExchange"
    />
</template>
<script setup lang="ts">
import { ref } from 'vue';
const visible = ref(false);
const emits = defineEmits(['call']);
const onEmitSelectSuperiod = () => { // 省略
  emits('call');
};
const onOpen = () => {
  visible.value = true;
};
const onClose = () => {
  visible.value = false;
};

defineExpose({
  onOpen,
  onClose,
});
</script>

那麼在父元件中,我們只需要透過ref得到該元件Dom,然後操作Dom內部的方法即可;

如:父元件改寫

<template>
  <exchange-valid-modal-vue ref="exchangeRef" />
</template>
<script setup lang="ts">
// 轉讓管理員元件
import ExchangeValidModalVue from './modal/ExchangeValidModal.vue';
// modal彈層
const exchangeRef = ref(null); // 轉讓管理員視覺化
const onExchangeOpen = () => {
   exchangeRef.onOpen() // 直接操作dom裡defineExpose暴露出來的方法
};
</script>

如此,是不是比父子單向資料傳遞的方式更加高效易用?當然上面所說的只是我舉的一個例子,當後續需要在元件內擴充套件功能也可按類似的方法代替單向資料流的方式擴充套件

但,請注意;這種操作dom方式,並不是什麼場景下都是最佳的選擇;我們可以分情況選擇,比如當一些資料只需要在子元件的範疇中實現,而不需要父元件外加干涉的情況下,選擇ref操作dom更為高效;

補充知識點:

defineExpose

在 Vue3.2 中,預設不會暴露任何在 <script setup> 中宣告的繫結,即不能透過模板 ref 獲取到元件例項宣告的繫結。

Vue3.2 提供了 defineExpose 編譯器宏,可以顯式地暴露需要暴露的元件中宣告的變數和方法

相關文章