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-bind
prop傳遞到自定義組件的變量名不是默認的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>
目標達成。