2

在日常的开发中,自定义一个支持双向绑定的组件是非常常见的应用场景,而官方文档中对于自定义组件如何实现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,且foobar的值都会被修改为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>双向绑定)时,触发currentValueset()函数,在set()中我们不直接修改任何值,而是$emit事件,由父组件修改原始绑定数据(父组件中的v-model实现),从而触发子组件中currentValue的get(),实现数据同步,完成双向绑定的一个循环。

总结

可以看到,当双向绑定的mixin编写完成后,我们在自定义组件中实现双向绑定仅需要引入此mixin即可,因此在此大言不惭其为最简洁的实现。
当然现在实现双向绑定还可以使用v-bindsync修饰符,在此就不做过多讨论了。

而值得注意的是,我们使用了计算属性来处理双向绑定,因此:

任何时候,在子组件若想触发双向绑定,一定要currentValue进行操作,而非操作其他变量

更多关于vue中model属性(不是v-model哦,参阅官方文档)的使用,及如何在ts中对currentValue的类型进行申明的内容,请参阅:
自定义组件v-model的最简洁实现 - 进阶篇(建设中,敬请期待)


枫凝烈
6 声望1 粉丝