从vue2到vue3,自定义组件的v-model实现原理

发布时间 2023-07-25 09:55:08作者: 秋闻道

前言

相信使用vue开发的同学应该都体会过v-model的便利,它可以非常方便地进行双向数据绑定,只要重新输入内容,视图就会立刻发生改变。本文将着重介绍如何在自定义组件当中使用v-model,以及在vue2和vue3中使用方式上的差异。

概述

v-model是一个语法糖,它在组件使用时相当于如下简写:

// vue2 原生组件
<input v-model="val" />
// 等价于
<input :value="val" @input="val = $event.target.value" />

要让组件的v-model生效,需要接收一个value属性,并在有新的value时触发input事件。以上面代码为例,绑定value属性到名为val的响应式对象,然后在input触发的时候绑定一个函数,每次input的值改变就会更新val,从而实现数据更新。

vue2实现方式

同理,自定义组件要如何支持v-model?先说说vue2的实现思路:

// vue2 自定义组件
<my-component v-model="val" />
// 等价于
<my-component :value="val" @input="val = arguments[0]" />

在MyComponent这个组件上面创建一个v-model,它的实际执行就是value的属性,之后触发input的事件,value接收的值就是事件回调函数的第一个参数。所以在自定义组件中实现事件绑定,我们需要使用$emit去触发input事件。

// MyComponent.vue
<template>
  <input type="text" :value="value" @input="updateInput" />
</template>

<script>
export default {
  props: {
    value: String,
  },
  methods: {
    updateInput(e) {
      this.$emit("input", e.target.value);
    },
  },
};
</script>

以上是常规组件的实现方法,那么面对一些不同寻常的组件又该如何应对呢?

用vue2的方式实现数据绑定的解决方案会出现这么一个问题:vue2的普通组件会默认使用value的属性名和input的事件。但是当在如checked这种单选框、复选框等类型的输入控件可能会将value属性用于不同的目的,不能用来指代当前的状态。如下在使用的属性是checked而非value来表示是否选中,改变的值使用的事件是change而非input,针对这种不走寻常路的组件,vue2的解决方案是添加一个model字段,里面有两个属性,prop表示想要绑定的属性,event表示触发事件的名称。

// BaseCheckbox.vue
<template>
  <input type="checkbox" :checked="checked" @change="updateInput">
</template>

<script>
export default {
  props: {
    checked: Boolean
  },
  model: {
    prop: 'checked',
    event: 'change'
  },
  methods: {
    updateInput(e) {
      this.$emit('change', e.target.checked)
    }
  }
}
</script>
// App.vue
<template>
  <div id="app">
    <my-component v-model="val"></my-component>
    <base-checkbox v-model="checked"></base-checkbox>
    <h1>{{ val }}</h1>
    <h1>{{ checked }}</h1>
  </div>
</template>

<script>
import MyComponent from "./components/MyComponent";
import BaseCheckbox from "./components/BaseCheckbox";

export default {
  name: "App",
  components: {
    MyComponent,
    BaseCheckbox,
  },
  data() {
    return {
      val: '',
      checked: false
    }
  }
};
</script>

vue3实现方式

那么vue2的实现方式是否已经完美的呢?显然不是,它还有以下明显的缺点:

  • 繁琐:需要新建model属性
  • 只能支持一个v-model:组件中可能会出现需要使用多个v-model双向绑定的场景
  • 理解困难:在不同的应用场景(如input输入框和check复选框)需要使用不同的方式

针对这些问题,vue3也给出了新的解决方案:

// vue3
<my-component v-model="foo" />
h(Comp, {
	modelValue: foo,
	'onUpdate:modelValue': value => (foo = value)
})

直接移除了组件上的model属性,不再使用value和input这两个非常容易混淆的属性和事件,换成了属性名称modelValue和更加详细的事件名称onUpdate:modelValue,换言之,要在vue3的自定义组件中使用v-model,首先需要有modelValue属性,然后需要在更新的时候触发onUpdate:modelValue事件。以下是vue3改造后的代码:

// App.vue
<template>
  <div id="app">
    <my-component v-model="inputVal"></my-component>
    <base-checkbox v-model="checkVal"></base-checkbox>
    <h1>{{ inputVal }}</h1>
    <h1>{{ checkVal }}</h1>
  </div>
</template>

<script>
import { defineComponent, ref } from "vue";
import MyComponent from "./components/MyComponent";
import BaseCheckbox from "./components/BaseCheckbox";
export default defineComponent({
  components: {
    MyComponent,
    BaseCheckbox,
  },
  setup() {
    const inputVal = ref("test");
    const checkVal = ref(false);
    return {
      inputVal,
      checkVal,
    };
  },
});
</script>
// MyComponent.vue
<template>
  <input type="text" :value="inputRef.val" @input="updateInput" />
</template>

<script>
import { defineComponent, reactive } from "vue";
export default defineComponent({
  props: {
    modelValue: String,
  },
  setup(props, context) {
    const inputRef = reactive({
      val: props.modelValue || "",
    });
    const updateInput = (e) => {
      const targetVal = e.target.value;
      inputRef.val = targetVal;
      context.emit("update:modelValue", targetVal);
    };
    return {
      inputRef,
      updateInput,
    };
  },
});
</script>
// BaseCheckbox.vue
<template>
  <input type="checkbox" :checked="checkedRef.val" @change="updateCheck" />
</template>

<script>
import { defineComponent, reactive } from "vue";
export default defineComponent({
  props: {
    checkedValue: Boolean,
  },
  setup(props, context) {
    const checkedRef = reactive({
      val: props.checkedValue,
    });
    const updateCheck = (e) => {
      const targetVal = e.target.checked;
      checkedRef.val = targetVal;
      context.emit("update:modelValue", targetVal);
    };
    return {
      checkedRef,
      updateCheck,
    };
  },
});
</script>

结语

本文讲述了v-model的原理、以及在vue2和vue3中的实现方式,代码的验证可以在codesandbox上来去进行,可以直接选择vue2或者vue3的运行环境,无需本地配置。