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