1. v-model 語法糖
當你希望一個自定義組件的值能夠實現雙向綁定。 那么就需要:
- 將值傳入組件;
- 將變化的值逆傳回父組件。
實際上,就可以利用 props 實現的父傳子 + 通過自定義事件this.$emit實現的子傳父。實現雙向的數據流傳遞。
下面是一個示例:
有這樣一個父組件:
<template>
<div>
<Child :cusProp="message" @cusEvent="message = $event" />
文字:{{message}}
</div>
</template>
<script>
import Child from "./comps/child.vue"
export default {
components: {
Child
},
data() {
return {
message: 'init default'
}
}
}
</script>
和這樣的一個子組件:
<template>
<div>
this is child comp
<input type="text" :value="cusProp" @input="onInputChange">
</div>
</template>
<script>
export default {
props:["cusProp"],
methods: {
onInputChange(e) {
this.$emit('cusEvent', e.target.value)
}
}
}
</script>

我們自定義了一個組件,名為<Child /> , 我們通過 v-bind:cusProp 向<Child /> 傳遞了一個名為 "cusProp" 的prop , 即 <Child :cusProp="message" /> 。
然后在<Child />組件內部,通過props接收到了這個值,並通過v-bind:cusProp 將值綁定給了<input /> 元素。
緊接着,我們給<input /> 元素設定了一個input 監聽事件, 當輸入時,觸發該事件,然后將當前值通過this.$emit('cusProp',e.target.value) 觸發了一個我們自定義命名為"cusProp"的自定義事件,以參數的形式,將變化后的值逆向傳遞(子傳父)給了父組件。 在父組件中接收到變化后的值,然后通過$event 將值賦給了綁定的 message 。
從而實現了自定義的雙向綁定。
實際上,上邊這個過程,可以簡化為一個vue為我們預定義實現的v-model, 但是不能直接替換,我們需要做一些簡單的處理。這就涉及到了自定義v-model
2. 自定義v-model
2.1 v-model 語法糖, 以及最簡單的自定義v-model
首先,我們仿照着vue文檔的舉例,嘗試去理解需要自定義v-model的使用場景。
文檔中有這樣一段描述很重要
一個組件上的
v-model默認會利用名為value的 prop 和名為input的事件
文檔中的這段話極為概要,但是這句話蘊含了很重要的一些細節:
實際,當你使用v-model 的時候,默認是,是傳遞的名為value的prop ,且$emit觸發的自定義事件的事件名是input 。
而回頭看看我們剛才寫的組件:
父組件:
<Child :cusProp="message" @cusEvent="message = $event" />
子組件:
<input type="text" :value="cusProp" @input="onInputChange">
...
props:["cusProp"]
...
onInputChange(e) {
this.$emit('cusEvent', e.target.value)
}
我們默認傳遞的prop值名為"cusProp", 即v-bind:cusProp , 且$emit觸發的自定義事件名為cusEvent 。 並不滿足能直接寫作v-model 的形式其前提條件。 所以我們不能直接替換。
我們需要做一些簡單的變化:
父組件:
<template>
<div>
<Child :value="message" @input="message = $event" /> <!--改動行-->
文字:{{message}}
</div>
</template>
<script>
import Child from "./comps/child.vue"
export default {
components: {
Child
},
data() {
return {
message: 'init default'
}
}
}
</script>
子組件:
<template>
<div>
this is child comp
<input type="text" :value="value" @input="onInputChange"> <!--改動行-->
</div>
</template>
<script>
export default {
props:["value"],// --改動行--
methods: {
onInputChange(e) {
this.$emit('input', e.target.value) //--改動行--
}
}
}
</script>
我們把prop 值的改為了value, 把$emit 觸發的事件改為了input 現在,我們就能寫作v-model 的 形式了,保持子組件不變,直接替換父組件中即可:
<Child v-model="message" />
自此,我們便能夠理解,為什么說v-model 實際上就是props + $emit 自定義事件的語法糖 。
2.2 通用自定義v-model
上邊的示例中,我們由於不滿足先是利用了props 父傳子,和自定義事件的子傳父,手動實現了一個數據流的雙向綁定。
緊接着,我們介紹了v-model 的實質,就是props + 自定義事件 的語法糖。 然后我們期望將我們自己的手動實現,簡化成v-model語法糖的形式。
文檔告訴我們,需要滿足兩個基本的默認條件:
prop名默認須為value;$emit觸發的自定義事件名默認須為input
而我們的手動實現起初並不滿足要求(prop ---- cusProp, $emit ---- cusEvent), 所以我們做了部分修改,以滿足默認的條件。 從而實現了將手動實現,轉換成了v-model語法糖的形式。
但是,這里有一個問題,就是v-model 默認的兩個條件,會對我們有着很大的限制,這里封裝的是一個<input/>輸入框,以value prop值,以input 作為自定義事件名,本身是合乎習慣的,但是,日常開發中,我們不可能只封裝一個輸入框,可不能所有的自定義v-model 組件,都以value 傳遞,自定義事件一定名為input ,這顯然是不合理的,也有違“自定義事件” 。我們開發工作中,可能更多的需要自定義指定prop名,和自定義事件。 為了更好的說明這個問題,解決通用性,下面我們通過一個示例來加深了解:
這里之所以要着重強調,是因為很容易出錯,這個文檔中說的
input事件到底指的是,自定義子組件中元素的監聽事件名為input,還是說$emit觸發的事件名為input。 以上就是為了強調,是后者,是$emit觸發的事件名,默認情況下,必須為input。 盡管它是自定義事件名,這也是之所以容易出錯的地方。
這里我們同樣使用<input/ 這個元素,但是,不再用輸入框了(type="text") ,我們將其指定為一個checkbox 看看會怎么樣呢?
<!--Father Component-->
<template>
<div>
<Child :cusProp="status" @cusEvent="status = $event" />
狀態:{{status}}
</div>
</template>
<script>
import Child from "../cusVModelcheckBox/comps/child.vue"
export default {
components: {
Child
},
data() {
return {
status: true
}
}
}
</script>
<!-- Child Component-->
<template>
<div>
this is child comp
<input type="checkbox" :checked="cusProp" @change="onChange">
</div>
</template>
<script>
export default {
props:["cusProp"],
methods: {
onChange(e) {
this.$emit('cusEvent', e.target.checked)
}
}
}

一樣的,如果此時,你想寫作v-model語法糖的形式。就需要想剛才那樣做一些改動:
父組件:
<template>
<div>
<Child v-model="status" /> <!--改動行-->
狀態:{{status}}
</div>
</template>
<script>
import Child from "../cusVModelcheckBox/comps/child.vue"
export default {
components: {
Child
},
data() {
return {
status: true
}
}
}
</script>
子組件:
<template>
<div>
this is child comp
<input type="checkbox" :checked="value" @change="onChange"> <!--改動行-->
</div>
</template>
<script>
export default {
props:["value"], //--改動行--
methods: {
onChange(e) {
this.$emit('input', e.target.checked) //--改動行--
}
}
}
現在,由於這是一個checkbox ,我們可以放大剛才所描述的限制了。 你莫名奇妙的加上接受了一個名為value 的prop, 以及通過$emit 觸發了一個莫名其妙的input自定義事件。 盡管它能夠如期的正常工作。 當代碼量多了之后, 你會發現這種組件異常難以維護。
那么到底該怎么解決這樣一種場景呢?
其實非常簡單 , 只需要指定一個model 對象屬性即可:
我們只需要在剛才的基礎上,在<Child/>組件中指定如下model配置加以稍微改動即可:
<template>
<div>
this is child comp
<input type="checkbox" :checked="cusProp" @change="onChange"> <!--改動行-->
</div>
</template>
<script>
export default {
props:["cusProp"], //改動行 //當然一般直接寫父組件v-model的變量名, 這里為了說明是任意名所以 寫了個cusProp
model:{ //改動行
prop:'cusProp', //改動行
event:'cusEvent' //改動行
},//改動行
methods: {
onChange(e) {
this.$emit('cusEvent', e.target.checked) //改動行
}
}
}
vue 為我們提供了一個名為model的實例配置項, 它可以指定一個任意的變量名,用於接受父組件中v-model 的傳遞值, 還可以指定一個任意的事件名, 用以"代理", $emit的觸發事件.
這樣,就解決了難以后期維護的問題,使得有雙向綁定需求的組件封裝更加的通用.
3. 總結
所以,總結一下。
什么情況下需要自定義v-model?
-
當有自定義組件的雙向數據流的需求的時候,都可以自定義
v-model來達成目的。-
其中,什么時候需要配置
model屬性?當默認通過
v-bindprop傳遞到自定義組件的變量名不是默認的value,或者 觸發自定義事件的事件名不為input的時候。 -
什么時候不需要配置
model屬性?當滿足默認的
v-model規則時,即 prop傳遞到自定義組件的變量名為value且 觸發自定義事件的事件名為input的時候,不需要指定model屬性配置。可直接使用v-model這種情況比較少見,基本僅當自定義組件是為了擴展type="text"的<input/>元素時才符合條件。
-
特別注意的一點:
自定義事件內部,可以通過任意事件去觸發$emit ,但是一般是通過DOM監聽事件,例如@change, @input,@click,等等。 但是默認情況下,如果不配置model實例配置,加以指明,$emit 觸發的事件名須是"input" 。 主要是不要混淆,這么默認情況下的約束規則,input 事件,指的是$emit觸發的事件名,而不是自定義子組件內部觸發$emit 的事件。
通過model 實例配置,實際上幫我們解決的主要問題是日后的維護問題,和代碼易讀性。 它相當於背后幫我們自動將默認prop為value ,默認自定義事件為input 做了一層別名化處理(alias),從而讓我們能夠去自定義任何名稱。
4. 附加拓展,實踐一個常見的v-model業務需求
【需求:】
假設現在有這樣一個需求(基於antdv)

有這樣一個區域級聯選擇器,我希望,我能從父組件中給它一個初始值cascaderSelected:"浙江/杭州"。 在級聯選擇器值變換以后,這個cascaderSelected值響應式的變化。 要求利用v-model 實現,從而讓代碼簡潔高效。
【實現:】
父組件:
<template>
<div>
<cus-area-cascader v-model="cascaderSelected"/>
當前選中區域:{{cascaderSelected}}
</div>
</template>
<script>
import CusAreaCascader from "../cusVModelPractice/comps/CusAreaCascader.vue"
export default {
components: {
CusAreaCascader
},
data() {
return {
cascaderSelected: ['zhejiang', 'hangzhou','xihu']
}
}
}
</script>
子組件:
<template>
<a-cascader :options="options" :value="onPropHandle" placeholder="Please select" @change="onChange" />
</template>
<script>
export default {
props:['onPropHandle'],
model:{
prop:'onPropHandle',
event:'onChangeHandle'
},
data() {
return {
options: [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
},
],
},
],
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
},
],
},
],
},
],
};
},
methods: {
onChange(value) {
this.$emit('onChangeHandle',value)
},
},
};
</script>

目標達成。
