一文搞懂 Vue3 defineModel 雙向綁定:告別繁瑣代碼!

前言

隨着vue3.4版本的發佈,defineModel也正式轉正了。它可以簡化父子組件之間的雙向綁定,是目前官方推薦的雙向綁定實現方式。

vue3.4以前如何實現雙向綁定

大家應該都知道v-model只是一個語法糖,實際就是給組件定義了modelValue屬性和監聽update:modelValue事件,所以我們以前要實現數據雙向綁定需要給子組件定義一個modelValue屬性,並且在子組件內要更新modelValue值時需要emit出去一個update:modelValue事件,將新的值作爲第二個字段傳出去。

我們來看一個簡單的例子,父組件的代碼如下:

<template>
  <CommonInput v-model="inputValue" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const inputValue = ref();
</script>

子組件的代碼如下:

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup lang="ts">
const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
</script>

上面的例子大家應該很熟悉,以前都是這樣去實現v-model雙向綁定的。但是存在一個問題就是input輸入框其實支持直接使用v-model的,我們這裏卻沒有使用v-model而是在input輸入框上面添加value屬性和input事件。

原因是因爲從vue2開始就已經是單向數據流,在子組件中是不能直接修改props中的值。而是應該由子組件中拋出一個事件,由父組件去監聽這個事件,然後去修改父組件中傳遞給props的變量。如果這裏我們給input輸入框直接加一個v-model="props.modelValue",那麼其實是在子組件內直接修改props中的modelValue。由於單向數據流的原因,vue是不支持直接修改props的,所以我們才需要將代碼寫成上面的樣子。

使用defineModel實現數據雙向綁定

defineModel是一個宏,所以不需要從vue中import導入,直接使用就可以了。這個宏可以用來聲明一個雙向綁定 prop,通過父組件的 v-model 來使用。

基礎demo

父組件的代碼和前面是一樣的,如下:

<template>
  <CommonInput v-model="inputValue" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const inputValue = ref();
</script>

子組件的代碼如下:

<template>
  <input v-model="model" />
</template>

<script setup lang="ts">
const model = defineModel();
model.value = "xxx";
</script>

在上面的例子中我們直接將defineModel的返回值使用v-model綁定到input輸入框上面,無需定義 modelValue 屬性和監聽 update:modelValue 事件,代碼更加簡潔。defineModel的返回值是一個ref,我們可以在子組件中修改model變量的值,並且父組件中的inputValue變量的值也會同步更新,這樣就可以實現雙向綁定。

那麼問題來了,從vue2開始就變成了單向數據流。這裏修改子組件的值後,父組件的變量值也被修改了,那這不就變回了vue1的雙向數據流了嗎?其實並不是這樣的,這裏還是單向數據流,我們接下來會簡單講一下defineModel的實現原理。

實現原理

defineModel其實就是在子組件內定義了一個叫model的ref變量和modelValue的props,並且watch了props中的modelValue。當props中的modelValue的值改變後會同步更新model變量的值。並且當在子組件內改變model變量的值後會拋出update:modelValue事件,父組件收到這個事件後就會更新父組件中對應的變量值。

實現原理代碼如下:

<template>
  <input v-model="model" />
</template>

<script setup lang="ts">
import { ref, watch } from "vue";

const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
const model = ref();

watch(
  () => props.modelValue,
  () => {
    model.value = props.modelValue;
  }
);
watch(model, () => {
  emit("update:modelValue", model.value);
});
</script>

看了上面的代碼後你應該瞭解到了爲什麼可以在子組件內直接修改defineModel的返回值後父組件對應的變量也會同步更新了吧。我們修改的其實是defineModel返回的ref變量,而不是直接修改props中的modelValue。實現方式還是和vue3.4以前實現雙向綁定一樣的,只是defineModel這個宏幫我們將以前的那些繁瑣的代碼給封裝到內部實現了。

其實defineModel的源碼中是使用 customRefwatchSyncEffect 去實現的,我這裏是爲了讓大家能夠更容易的明白defineModel的實現原理才舉的refwatch的例子。如果大家對defineModel的源碼感興趣,請在評論區留言,如果感興趣的小夥伴比較多,我會在下一期出一篇defineModel源碼的文章。

defineModel如何定義typedefault

既然defineModel是聲明瞭一個prop,那同樣也可以定義prop的typedefault。具體代碼如下:

const model = defineModel({ type: String, default: "20" });

除了支持typedefault,也支持requiredvalidator,用法和定義prop時一樣。

defineModel如何實現多個   v-model   綁定

同樣也支持在父組件上面實現多個   v-model   綁定,這時我們給defineModel傳的第一個參數就不是對象了,而是一個字符串。

const model1 = defineModel("count1");
const model2 = defineModel("count2");

在父組件中使用v-model時代碼如下:

<CommonInput v-model:count1="inputValue1" />
<CommonInput v-model:count2="inputValue2" />

我們也可以在多個v-model中定義typedefault

const model1 = defineModel("count1", {
  type: String,
  default: "aaa",
});

defineModel如何使用內置修飾符和自定義修飾符

如果要使用系統內置的修飾符比如trim,父組件的寫法還是和之前是一樣的:

<CommonInput v-model.trim="inputValue" />

子組件也無需做任何修改,和上面其他的defineModel例子是一樣的:

const model = defineModel();

defineModel也支持自定義修飾符,比如我們要實現一個將輸入框的字母全部變成大寫的uppercase自定義修飾符,同時也需要使用內置的trim修飾符。

我們的父組件代碼如下:

<CommonInput v-model.trim.uppercase="inputValue" />

我們的子組件需要寫成下面這樣的:

<template>
  <input v-model="modelValue" />
</template>

<script setup lang="ts">
const [modelValue, modelModifiers] = defineModel({
  // get我們這裏不需要
  set(value) {
    if (modelModifiers.uppercase) {
      return value?.toUpperCase();
    }
  },
});
</script>

這時我們給defineModel傳進去的第一個參數就是包含get 和 set 方法的對象,當對modelValue變量進行讀操作時會走到get方法裏面去,當對modelValue變量進行寫操作時會走到set方法裏面去。如果只需要對寫操作進行攔截,那麼可以不用寫get

defineModel的返回值也可以解構成兩個變量,第一個變量就是我們前面幾個例子的ref對象,用於給v-model綁定。第二個變量是一個對象,裏面包含了有哪些修飾符,在這裏我們有trimuppercase兩個修飾符,所以modelModifiers的值爲:

{
  trim: true,
  uppercase: true
}

在輸入框進行輸入時,就會走到set方法裏面,然後調用value?.toUpperCase()就可以實現將輸入的字母變成大寫字母。

總結

這篇文章介紹瞭如何使用defineModel宏實現雙向綁定以及defineModel的實現原理。

  • 在子組件內調用defineModel宏會返回一個ref對象,在子組件內可以直接對這個ref對象進行賦值,父組件內的相應變量也會同步修改。
  • defineModel其實就是在子組件內定義了一個ref變量和對應的prop,然後監聽了對應的prop保持ref變量的值始終和對應的prop是一樣的。在子組件內當修改ref變量值時會拋出一個事件給父組件,讓父組件更新對應的變量值,從而實現雙向綁定。
  • 使用defineModel({ type: String, default: "20" })就可以定義prop的typedefault等選項。
  • 使用defineModel("count")就可以實現多個v-model綁定。
  • 通過解構 defineModel() 的返回值拿到modelModifiers修飾符對象,配合 get 和 set 轉換器選項實現自定義修飾符。

如果我的文章對你有點幫助,歡迎關注公衆號:【歐陽碼農】,文章在公衆號首發。你的支持就是我創作的最大動力,感謝感謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章