在日常的开发中,自定义一个支持双向绑定的组件是非常常见的应用场景,而官方文档中对于自定义组件如何实现v-model
双向绑定的描述几近于0。那么,怎样实现一个自定义组件v-model
,且能够使用轻便、简洁,就是本篇将要讨论的内容。
知识准备
v-model 语法糖
我们知道,vue中的v-model
指令是一组语法糖,
<input v-model="name">
等价于
<input :value="name" @input="value = arguments[0]">
计算属性的get与set
我们知道,vue中的计算属性是可以拆分get()
、set()
的。
在默认不拆分书写的情况下,相当于只有get()
函数,即该计算属性只读,不应直接修改。这一点在使用typescript书写时更加直观。
而一旦书写了set()
,则在尝试修改该计算属性值时,会触发set()
函数的执行。
例如,js写法:
export default {
data() {
return {
foo: 1
};
},
computed: {
bar: {
get() {
return this.foo;
},
set(newVal) {
console.log(newVal);
this.foo = newVal;
}
}
},
mounted(){
this.bar = 2;
}
};
ts写法:
@Component
export default class TestComponent extends Vue {
foo = 1;
get bar() {
return this.foo;
}
set bar(newVal: number) {
console.log(newVal);
this.foo = newVal;
}
mounted() {
this.bar = 2;
}
}
上述组件挂载后,将在控制台打印2
,且foo
和bar
的值都会被修改为2。
实现
综合上述特性,我们可以认为我们要实现自定义组件的双向绑定,其实需要的功能其实是:
- 组件内部可以接收并同步父组件传入的value值
- 组件内部可以在该双向绑定值修改时
emit
一个input
事件
我们知道,直接修改父组件传入的值(prop)是不被允许的,
而且需要在双向绑定值于组件内部修改时拦截其操作,改为向父组件emit
事件,
那么使用计算属性的get()
、set()
来写再合适不过了。
且为了使其具有可复用性,我们可以将其抽离为一个mixin,则有:
JS写法
two-way.js ↓
export default {
prop: ['value'],
computed: {
currentValue: {
get() {
return this.value;
},
set(newVal) {
this.$emit('input', newVal);
}
}
}
}
my-child-compnent.vue ↓
<template>
<input v-model="currentValue">
</template>
<script>
import TwoWay from "path/to/two-way.js";
export default {
mixins: [TwoWay],
mounted() {
this.currentValue = 2;
}
};
</script>
parent-component.vue ↓
<template>
<children-component v-model="foo"></children-component>
</template>
<script>
export default {
data() {
return {
foo: 1
};
}
};
</script>
TS写法
two-way.ts ↓
import { Vue, Component, Prop } from 'vue-property-decorator'
@Component
export default class TwoWay extends Vue {
@Prop()
value!: any;
get currentValue() {
return this.value;
}
set currentValue(newVal: any) {
this.$emit('input', newVal)
}
}
my-child-compnent.vue ↓
<template>
<input v-model="currentValue" />
</template>
<script lang="ts">
import { Vue, Component, Mixins } from "vue-property-decorator";
import TwoWay from "path/to/two-way";
@Component
export default class MyChildComponent extends Mixins(TwoWay) {
mounted() {
this.currentValue = 2;
}
}
</script>
parent-component.vue ↓
<template>
<children-component v-model="foo"></children-component>
</template>
<script lang="ts">
import { Vue, Component } from "vue-property-decorator";
@Component
export default class ParentComponent extends Vue {
foo = 1;
}
</script>
上述代码实现了:子组件中input中的值修改时,父组件的foo
属性会同步修改。
当在子组件中修改currentValue
的值(mounted中js操作,或与<input>
双向绑定)时,触发currentValue
的set()
函数,在set()
中我们不直接修改任何值,而是$emit
事件,由父组件修改原始绑定数据(父组件中的v-model实现),从而触发子组件中currentValue的get()
,实现数据同步,完成双向绑定的一个循环。
总结
可以看到,当双向绑定的mixin编写完成后,我们在自定义组件中实现双向绑定仅需要引入此mixin即可,因此在此大言不惭其为最简洁的实现。
当然现在实现双向绑定还可以使用v-bind
的sync
修饰符,在此就不做过多讨论了。
而值得注意的是,我们使用了计算属性来处理双向绑定,因此:
任何时候,在子组件若想触发双向绑定,一定要
对currentValue
进行操作,而非操作其他变量。
更多关于vue中model
属性(不是v-model
哦,参阅官方文档)的使用,及如何在ts中对currentValue
的类型进行申明的内容,请参阅:
自定义组件v-model的最简洁实现 – 进阶篇(建设中,敬请期待)