[Vue深入组件]:v-model语法糖与自定义v-model
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>
目标达成。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步