
一、v-model 的使用原理
在 Vue 中,使用 v-bind 可以實作單向資料流,即父組件向子組件傳入基本資料型別,但反過來,子組件中不能修改父組件傳過來的基本資料型別,
想要實作資料的雙向傳遞,需要使用 Vue 提供的事件機制,即在子組件中通過 $emit() 觸發一個事件,在父組件中則需要使用對應的 v-on 屬性監聽對應的事件,并在事件發生時修改相應的資料,
Vue 將其簡化為了一個語法糖,即:
<input type="text" v-model="name">
本質上是:
<input type="text" :value="name" @input="name = $event.target.value">
而根據 Html 5 標準,對于<input>、<textarea> 、<select> 等原生的表單標簽,它們的屬性不一定都是 value ,發出的事件也不一定都是 input ,因此,Vue 為它們做了單獨的適配:
text和textarea元素使用value屬性和input事件;checkbox和radio使用checked屬性和change事件;select欄位將value作為prop并將change作為事件,
但對于除這些標簽以外的其他標簽,Vue 默認 會 系結 value 屬性 和 監聽 input事件,
基于此,只需要記住一個事實,v-model 只是同時完成 資料的系結 和 事件的監聽 而已,它內部實作的機理只是一個簡化書寫的語法糖,
二、自定義組件中的 v-model
了解了 v-model 的原理,我們可以想象,想要在自定義組件中實作 v-model,其實對應要做的就是 允許父組件進行資料系結 和 在資料發生變化時發出對應的事件 即可,
方法1. 拼湊默認的語法糖
既然對組件使用 v-model 時,Vue 默認 會 系結 value 屬性 和 監聽 input 事件,那么我們就可以依靠拼湊語法糖的方式在自定義組件上實作 v-model,
首先,我們擁有一個父組件 App.vue,其中包含一個子組件 Parent,它要實作的功能是一個帶有調節按鈕的數值選擇器:

父組件
App的參考代碼:
<template>
<div id="app">
<Parent v-model="parentValue"></Parent>
</div>
</template>
<script>
import Parent from './components/Parent.vue'
export default {
name: 'app',
data(){
return {
parentValue:5
}
},
components:{
Parent
}
}
</script>
現在,問題就只剩下如何在子組件Parent中拼湊出v-model的語法糖,
子組件的結構如下:
<template>
<div id="Parent">
<button @click="changeValue(-1)"> - </button>
{{value}}
<button @click="changeValue(+1)"> + </button>
</div>
</template>
要點一:允許父組件進行資料系結
因為 Vue 會 默認系結 value 屬性,因此我們在子組件的 props 中添加 value 欄位,
props:{
value:{
type: Number,
default: 5
}
},
要點二:允許父組件進行資料系結
因為 Vue 會 默認監聽 input 事件,因此在改變數值時,應當發出 input 事件,同時在事件中包裹新的值,以便父組件接收,
methods:{
changeValue(dv) {
this.$emit("input",this.value + dv)
}
}
通過以上簡單的兩步,我們就輕松地拼湊出了 v-model 的默認語法糖,從而實作了自定義組件中的 v-model,
子組件
Parent的參考代碼:
<template>
<div id="Parent">
<button @click="changeValue(-1)"> - </button>
{{value}}
<button @click="changeValue(+1)"> + </button>
</div>
</template>
<script>
export default{
name:"Parent",
props:{
value:{
type: Number,
default: 5
}
},
methods:{
changeValue(dv) {
this.$emit("input",this.value + dv)
}
}
}
</script>
方法2. 使用 model 欄位
拼湊默認的語法糖雖然可行,但顯然這并不是一種理想的方式,因為我們想要實作v-model的欄位不一定是value,如何為v-model自定義系結屬性和監聽事件呢?
假設我們現在不使用 value 屬性和 input 事件,而是使用了名為 num 的屬性 和 名為 numChanged 的事件,
新的子組件
Parent的參考代碼 - 1:
<template>
<div id="Parent">
<button @click="changeValue(-1)"> - </button>
{{num}}
<button @click="changeValue(+1)"> + </button>
</div>
</template>
<script>
export default{
name:"Parent",
props:{
num:{
type: Number,
default: 5
}
},
methods:{
changeValue(dv) {
this.$emit("numChanged",this.num + dv)
}
}
}
</script>
至此,默認的v-model就失效了,
這時候就需要請出我們的model欄位(僅限于 Vue 2.2.0+),
model:{
prop:"num",
event:"numChanged"
},
在子組件中添加如上的代碼段,就可以自定義 v-model 的 屬性 和 監聽事件,
如上,我們就通過使用 model 欄位完成了自定義組件的 v-model,
新的子組件
Parent的參考代碼 - 2:
<template>
<div id="Parent">
<button @click="changeValue(-1)"> - </button>
{{num}}
<button @click="changeValue(+1)"> + </button>
</div>
</template>
<script>
export default{
name:"Parent",
model:{
prop:"num",
event:"numChanged"
},
props:{
num:{
type: Number,
default: 5
}
},
methods:{
changeValue(dv) {
this.$emit("numChanged",this.num + dv)
}
}
}
</script>
三、在多層嵌套的組件中使用v-model
現在描述這樣一個情形:假設我有超組件App、父組件Parent和子組件Child,我需要通過v-model將App中的一個值 parentValue 一路系結至 Child,并實作同步,
假設直接使用剛才的方式,分別為Parent組件和Child組件實作自定義的v-model,這樣是否可行呢?
不妨做一個實驗:

我們分別為三個組件都準備一個值顯示幕和調節手柄,
在App中,調節手柄的主要作業為直接修改值:
methods:{
changeValue(dv){
this.parentValue += dv;
}
}
而在Parent 和 Child 兩個子組件中,調節手柄的作用與第二部分的自定義子組件v-model一致:
methods:{
changeValue(dv) {
this.$emit("numChanged",this.num + dv)
}
},
在這樣的狀況下,我們做下面三個操作:
- 點擊
App的調節手柄時,值的變化成功同步到了兩個子組件中, - 點擊
Parent的調節手柄時,值的變化也成功同步到了它的父組件和它的子組件中, - 點擊
Child的調節手柄時,出現了問題,

可以看到,值的變化只同步到了它的第一層父組件Parent,App中的值沒有發生變化,
同時,瀏覽器的控制臺報出如下錯誤:

原因就出在v-model的實作原理上,
在點擊子組件Child的調節手柄時,它會向父組件Parent發出一個事件:
this.$emit("numChanged",this.num + dv)
此時,父組件因為系結了v-model,所以會接收這個事件,而v-model是如下陳述句的語法糖:
<child :num="num" @numChanged="num = $event"></child>
而上述陳述句num = $event 修改了 prop 中屬性的值,這違背了Vue的設計原則,同時parent組件也沒有再進一步向父組件App發出事件,導致值的修改沒有被同步到App,
解決方案
這個問題該如何解決?
不完美的解決方案:
很多朋友可能會首先想到,通過為parent組件中的值設定watch監聽器,在值變化時,向父組件App發出事件,完成同步,
watch:{
num:{
handler(newValue,oldValue) {
this.$emit("numChanged",newValue)
}
}
},
這樣確實實作了功能,因為如此推斷,當num發生變化時,parent確實能夠通過事件將變化傳給上一級,
但這么寫并不優雅,因為在底層,仍然是首先修改了props的值,然后才通知父組件修改相應的值,這仍然會引發Vue的警告,

完美的解決方案 1:
考慮v-model的底層實作機制,直接向下層組件通過v-model傳遞prop中的值,必然會引發賦值,因此,在v-model中必須傳遞一個非props值,
data(){
return {
myNum:5
}
},
我們在parent組件內安排一個新的data屬性myNum,并把它系結給v-model:
<template>
<div id="Parent">
Parent 中目前的值: {{num}}
<br>
Parent 的調節手柄:<button @click="changeValue(-1)"> - </button>
<button @click="changeValue(+1)"> + </button>
<child v-model="myNum"></child>
</div>
</template>
現在我們要做的,就是實作myNum和num的同步,
當下層組件給parent傳值時,myNum中的值會發生變化,我們通過watch監聽變化,并將變化傳遞給上層組件,由上層組件修改num的值(從而避免直接修改props):
myNum:{
handler(newValue,oldValue) {
this.$emit("numChanged",newValue)
}
}
同時,當上層組件的num發生變化時,我們也需要同步myNum的值:
num:{
handler(newValue,oldValue) {
this.myNum = this.num;
}
},
基本完成了,但還有一個注意點,在Vue組件完成掛載時,myNum和num的值可能不同步,這并不會被watch監聽到,因此我們還需要提供一個鉤子:
mounted(){
this.myNum = this.num
},
通過這一系列操作,我們用一個data屬性代替了props屬性,從而避免了警告,
新的子組件
Parent的參考代碼 - 3:
<template>
<div id="Parent">
Parent 中目前的值: {{num}}
<br>
Parent 的調節手柄:<button @click="changeValue(-1)"> - </button>
<button @click="changeValue(+1)"> + </button>
<child v-model="myNum"></child>
</div>
</template>
<script>
import child from './Child.vue'
export default{
name:"Parent",
model:{
prop:"num",
event:"numChanged"
},
props:{
num:{
type: Number,
default: 5
}
},
data(){
return {
myNum:5
}
},
methods:{
changeValue(dv) {
this.$emit("numChanged",this.num + dv)
}
},
watch:{
num:{
handler(newValue,oldValue) {
this.myNum = this.num;
}
},
myNum:{
handler(newValue,oldValue) {
this.$emit("numChanged",newValue)
}
}
},
mounted(){
this.myNum = this.num
},
components:{
child
}
}
</script>
完美的解決方案 2:
在上一個解決方案中,我們選擇使用watch的監聽方法,用一個data代替prop,但我們為了實作這個功能,增加了data、watch和mounted三個欄位,確實令人困惑,
事實上,可以只使用一個計算屬性computed,來完成實作這些功能:
computed:{
myNum:{
get(){
return this.num;
},
set(newValue){
this.$emit("numChanged",newValue)
}
}
},
新的子組件
Parent的參考代碼 - 4:
<template>
<div id="Parent">
Parent 中目前的值: {{num}}
<br>
Parent 的調節手柄:<button @click="changeValue(-1)"> - </button>
<button @click="changeValue(+1)"> + </button>
<child v-model="myNum"></child>
</div>
</template>
<script>
import child from './Child.vue'
export default{
name:"Parent",
model:{
prop:"num",
event:"numChanged"
},
props:{
num:{
type: Number,
default: 5
}
},
methods:{
changeValue(dv) {
this.$emit("numChanged",this.num + dv)
}
},
computed:{
myNum:{
get(){
return this.num;
},
set(newValue){
this.$emit("numChanged",newValue)
}
}
},
components:{
child
}
}
</script>
總結
這一部分內容中,我們介紹了如何在在多層嵌套的組件中使用v-model ,在這種情況下,除了最頂層組件和最底層組件,其他的中間組件都需要使用data代替props,我們推薦使用解決方案2,因為它比較優雅且易讀,
四、v-model的一些其他使用細節
1. 多選與單選

當我們為多選的 select 標簽系結 v-model 時,得到的會是一個陣列,
復選的 checkbox 同理,
多選的
select的參考代碼:
<template>
<div id="app">
{{mySelects}}
<select v-model="mySelects" multiple>
<option value="apple">蘋果</option>
<option value="banana">香蕉</option>
<option value="orange">橘子</option>
</select>
</div>
</template>
但當我們為多個單選的radio標簽系結v-model時,得到的只是單選的值,

單選的
radio的參考代碼:
<template>
<div id="app">
<div id="app">
<label for="male">
<input type="radio" id="male" value="男" v-model="gender"> 男
</label>
<label for="female">
<input type="radio" id="female" value="女" v-model="gender"> 女
</label>
<h2>您選擇的性別是:{{gender}}</h2>
</div>
</div>
</template>
2. 修飾符
在使用 v-model 時,在后面加上 “.[修飾符]” ,即可實作一些特殊的功能,
lazy修飾符:
默認情況下,v-model默認是在input事件中同步輸入框的資料的,
也就是說,一旦有資料發生改變對應的data中的資料就會自動發生改變,
lazy修飾符可以讓資料在失去焦點或者回車時才會更新:number修飾符:
默認情況下,在輸入框中無論我們輸入的是字母還是數字,都會被當做字串型別進行處理,
但是如果我們希望處理的是數字型別,那么最好直接將內容當做數字處理,
number修飾符可以讓在輸入框中輸入的內容自動轉成數字型別,trim修飾符:
如果輸入的內容首尾有很多空格,通常我們希望將其去除,trim修飾符可以過濾內容左右兩邊的空格,
修飾符
lazy的參考代碼:
<div id="app">
<input type="text" v-model.lazy="content" placeholder="請輸入">
<p>輸入框:{{content}}</p>
</div>
<script>
new Vue({
el: '#app',
data: {
name: '123',
content: ''
}
})
</script>
五、總結
v-model 作為一個語法糖,在 Vue 中有著非常重要的地位,合理、有效地使用 v-model 可以有效提升專案代碼的可讀性,
本專欄將持續更新,關注我,繼續帶你體驗更多 Vue 技巧!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/323459.html
標籤:其他
上一篇:vue3 + typescript + axios封裝(附帶loading效果,...并攜帶跨域處理,...element-plus按需引入)
