回顧從 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>
這個例子中,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
。
<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 兩個屬性,不考慮事件觸發,其實這就是兩個普通的屬性。
特殊之處在於,這裏在期望數據改變時,觸發 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
修飾符。
本質上就是以下寫法的語法糖
<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 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
在 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