vue v-model 雙向繫結

J.晒太阳的猫發表於2024-04-21

回顧從 vue2 到 vue3 v-model 雙向繫結的寫法變化

場景

v-model 雙向繫結,用於處理表單輸入繫結,類似於 react 中的受控元件。

// React 受控元件
function App() {
  const [text, setText] = useState("");

  return (
    <>
      <h3>{text}</h3>
      <input
        value={text}
        onInput={(e) => {
          setText(e.target.value);
        }}
      ></input>
    </>
  );
}

vue 的 v-model 本質與 react 受控元件是一樣的,只是加了一個語法糖封裝。

vue2 表單 v-model

<template>
  <div>
    <h2>FullName: {{ fullName }}</h2>
    <h3>Email: {{ email }}</h3>

    <input v-model="firstName" />
    <input :value="lastName" @input="(e) => (lastName = e.target.value)" />

    <input v-model.trim="email" placeholder="your email here" />
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      firstName: "",
      lastName: "",
      email: "",
    };
  },
  computed: {
    fullName() {
      return this.firstName + " " + this.lastName;
    },
  },
};
</script>

表單輸入繫結 — Vue.js

這個例子中,firstName 使用 v-model 的基礎寫法,lastName 是還原 v-model 的“本來面目”。
需要注意的是,這裡對 input 標籤,繫結的是 value 屬性和 input 事件,不同的 input 標籤型別,對應的屬性和事件不同,詳見官方文件。

email 資料新增了修飾符,可以做一些額外的處理

vue2 父子元件 v-model

下面這個案例展示對於自定義元件,如何使用 v-model。
在元件間使用 v-model,一個隱含的場景是,資料是由父元件提供的,子元件可能會修改資料,然後通知父元件更新資料。
不管是 vue 還是 react,都是單向資料流的設計,子元件不應該直接修改父元件給過來的資料,而是通知父元件,讓父元件處理,完成所謂的雙向繫結。

PS 如果資料本身就是子元件產生的,那直接透過事件告知父元件即可,這種場景沒有雙向繫結,也就不需要 v-model。

// Foo 元件,子元件

<template>
  <div>
    <!-- <input :value="value" @input="(e) => this.$emit('input', e.target.value)" /> -->
    <input
      :value="firstName"
      @input="(e) => this.$emit('updateFristName', e.target.value)"
    />

    <input
      :value="lastName"
      @input="(e) => this.$emit('update:lastName', e.target.value)"
    />

    <input
      :value="email"
      @input="(e) => this.$emit('update:email', e.target.value.trim())"
      placeholder="your email here"
    />

    <p>{{ firstName }} {{ lastName }} {{ email }}</p>
  </div>
</template>

<script>
export default {
  name: "FooItem",
  model: {
    prop: "firstName",
    event: "updateFristName",
  },
  props: {
    // value: String,
    firstName: String,
    lastName: String,
    email: {
      type: String,
      default: "https://www.cnblogs.com/jasongrass",
    },
  },
  data() {
    return {};
  },
};
</script>

這裡子元件中是沒有任何 v-model 這個指令的,因為 v-model 有兩個功能,一個是提供資料,一個是修改資料(在事件回撥中),而子元件是不能修改父元件提供的資料的,會破壞單向資料流。
所以這裡子元件只是透過 props 接受資料,需要修改資料時,只觸發事件,具體的事件處理和資料的實際修改,在父元件中完成。

具體寫法上,上面的子元件程式碼中,涉及到了三種寫法。

子元件 1. 預設寫法

在上面程式碼中被註釋的部分,即預設的資料名稱是 value,預設的事件名稱是 input

文件:自定義事件 — Vue.js

<input :value="value" @input="(e) => this.$emit('input', e.target.value)" />

子元件 2. 修改預設寫法

預設寫法有兩個問題,一是不夠語義化,在資料比較多的時候,value 具體的業務含義會很不直觀,影響程式碼可讀性;二是在其它場景下,可能不能滿足需求,如使用單選框、核取方塊等不同的表單元素時。

此時就可以自定義,如上面的 firstName,預設的 v-model 雙向繫結屬性名稱,變成了 firstName, 事件變成了 updateFristName。

model: {
  prop: "firstName",
  event: "updateFristName",
}

<input
  :value="firstName"
  @input="(e) => this.$emit('updateFristName', e.target.value)"
/>

子元件 3. 多個資料的雙向繫結

這裡就是 lastName 和 email 兩個屬性,不考慮事件觸發,其實這就是兩個普通的屬性。

修飾符 .sync — Vue.js

特殊之處在於,這裡在期望資料改變時,觸發 update:myPropName 事件,以通知父元件修改相關的資料。

<input
  :value="lastName"
  @input="(e) => this.$emit('update:lastName', e.target.value)"
/>

// FooContainer 元件,父元件

<template>
  <div>
    <h2>FullName: {{ fullName }}</h2>
    <h3>Email: {{ email }}</h3>
    <!-- :lastName.sync="lastName" -->
    <FooItem
      v-model="firstName"
      :lastName="lastName"
      @update:lastName="
        (e) => {
          lastName = e;
        }
      "
      :email.sync="email"
    ></FooItem>
  </div>
</template>

<script>
import FooItem from "./Foo.vue";
export default {
  name: "FooContainer",
  components: {
    FooItem,
  },
  data() {
    return {
      firstName: "",
      lastName: "",
      email: "",
    };
  },
  computed: {
    fullName() {
      return this.firstName + " " + this.lastName;
    },
  },
};
</script>

<style scoped></style>

父元件 1. 預設寫法

如上面的 firstName,如果需要將父元件中的 firstName 資料,作為子元件的預設 v-model 資料繫結,直接寫 v-model="firstName"
這樣就會實現與子元件預設 model 的雙向繫結

父元件 2. 修改預設寫法

修改預設寫法,是針對子元件而言的。對於父元件,只要是繫結子元件的 model(因為只有一個),寫法就是 v-model="firstName"

父元件 3. 多個資料的雙向繫結

如這裡的 lastName 和 email 資料,多個資料的繫結,可以對 v-bind 使用 .sync 修飾符。

.sync 修飾符 — Vue.js

本質上就是以下寫法的語法糖

    <FooItem
      :lastName="lastName"
      @update:lastName="
        (e) => {
          lastName = e;
        }
      "
    ></FooItem>

vue3 v-model 的變化

主要變化體現在自定義元件的 v-model 上,vue2 中一個元件只有一個 model 定義,其它的是透過 v-bind 的 .sync 修飾符來實現的。
在語法上容易混淆 v-model 和 v-bind 的用法,不是很直觀。

v-model | Vue 3 遷移指南

以下是對變化的總體概述:

  • 非相容:用於自定義元件時,v-model prop 和事件預設名稱已更改:
    prop:value -> modelValue;
    事件:input -> update:modelValue;
  • 非相容:v-bind 的 .sync 修飾符和元件的 model 選項已移除,可在 v-model 上加一個引數代替;
  • 新增:現在可以在同一個元件上使用多個 v-model 繫結;
  • 新增:現在可以自定義 v-model 修飾符。

vue3 表單 v-model

這部分沒有什麼變化,詳見文件:表單輸入繫結 | Vue.js

<template>
  <div>
    <div>
      <h2>FullName: {{ fullName }}</h2>
      <h3>Email: {{ email }}</h3>

      <input v-model="firstName" />
      <input :value="lastName" @input="(e) => (lastName = e.target.value)" />

      <input v-model.trim="email" placeholder="your email here" />
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, toRefs, computed } from "vue";

const info = reactive({
  firstName: "",
  lastName: "",
  email: "",
});
const { firstName, lastName, email } = toRefs(info);

const fullName = computed(() => {
  return firstName.value + " " + lastName.value;
});
</script>

vue3 父子元件 v-model

元件 v-model | Vue.js

在 vue 3.4 版本之後,使用了 defineModel 宏,處理 v-model 雙向繫結寫法上就簡單多了。

// Foo 元件,子元件

<template>
  <div>
    <input :value="model" @input="(e) => (model = e.target.value)" />
    <input v-model="model" />

    <input :value="lastName" @input="(e) => (lastName = e.target.value)" />
    <input v-model="lastName" />

    <input :value="email" @input="updateEmail" placeholder="your email here" />

    <p>{{ model }} {{ lastName }} {{ email }}</p>
  </div>
</template>

<script setup>
const model = defineModel();
const lastName = defineModel("lastName");
const [email, emailModifiers] = defineModel("email");

const updateEmail = (e) => {
  const inputValue = e.target.value;
  if (emailModifiers.upper) {
    console.log(inputValue);
    email.value = inputValue ? inputValue.toUpperCase() : "";
  } else {
    email.value = inputValue;
  }
};
</script>

<style lang="less" scoped></style>

子元件 1. 預設寫法

model 定義
const model = defineModel();

model 使用
<input v-model="model" />

預設寫法就是在使用 defineModel 時,不指定 model 的名稱,則內部預設名稱是 modelValue, 對應的更新事件名稱是 update:modelValue, 但這兩個預設名稱,都不需要體現在程式碼中。
程式碼中直接使用 defineModel 的返回值,可以自定義命名,如這裡是 model,它是一個 ref, 可以直接讀取或修改,如果是修改,則底層會自動呼叫 update:modelValue 事件,通知父元件處理。

注意,這裡在子元件中,可以直接使用 v-model,而不是必須寫成 <input :value="model" @input="(e) => (model = e.target.value)" /> 這樣手動繫結 value 和 觸發事件 的方式。因為這裡 v-model 繫結的是一個 ref 代理,內部在修改資料時,沒有真實修改資料,而是觸發事件。

在 vue3.4 之前,不支援這樣寫的時候,可以自定義一個計算屬性,將 input 標籤的 value 繫結到這個計算屬性中, 計算屬性的 get 方法中返回 model, 計算屬性的 set 方法中,觸發 update:modelValue 事件。但這樣還是需要手動新增並封裝一個計算屬性。

程式碼上省心很多,但這裡仍然遵守資料單向流的設計原則(雖然看起來像是直接在修改資料),如果父元件不對事件做處理(當然,通常父元件對事件的處理,也是被自動封裝在了 v-model 指令中),則子元件對資料的“修改”,也是無效的。

子元件 2&3. 修改預設寫法 和 多個 v-model

在使用 defineModel 之後,不管是預設寫法,還是定義多個 v-model,都進行了風格上的統一。直接使用 defineModel 定義即可。

const lastName = defineModel("lastName");

子元件,處理自定義修飾符

<script setup>
const [email, emailModifiers] = defineModel("email");

const updateEmail = (e) => {
  const inputValue = e.target.value;
  if (emailModifiers.upper) {
    console.log(inputValue);
    email.value = inputValue ? inputValue.toUpperCase() : "";
  } else {
    email.value = inputValue;
  }
};
</script>

// FooContainer 元件,父元件

<template>
  <div>
    <h2>FullName: {{ fullName }}</h2>
    <h3>Email: {{ email }}</h3>
    <Foo
      v-model="fristName"
      v-model:lastName="lastName"
      v-model:email.upper="email"
    ></Foo>
  </div>
</template>

<script setup>
import { computed, ref } from "vue";
import Foo from "./Foo.vue";

const fristName = ref("");
const lastName = ref("");
const email = ref("");

const fullName = computed(() => {
  return fristName.value + " " + lastName.value;
});
</script>

父元件的寫法也簡單直接了很多,對於預設 model, 直接使用 v-model="fristName" 這樣的方式繫結,對於其它命名的 model, 使用 v-model:lastName="lastName" 進行繫結。
v-model 內部自動處理了監聽子元件對應事件,並修改對應資料的操作。


總結

vue 3.4 之後,對 v-model 進行了很多最佳化,引入 defineModel 統一了 vue2 各種 model 的寫法,方便地支援了多個 v-model。
但仍然需要注意,本質上 v-model 還是沒有改變單向資料流這個設計原則,只是實現細節被封裝起來了,在開發中需要有這個意識。

參考文件

Vue2
表單輸入繫結 — Vue.js
元件 v-model | Vue.js
自定義元件的 v-model & .sync 修飾符 — Vue.js

Vue3
v-model | Vue 3 遷移指南
表單輸入繫結 | Vue.js
元件 v-model | Vue.js

原文連結:https://www.cnblogs.com/jasongrass/p/18148695

相關文章