vue v-model 雙向綁定

回顧從 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

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