Published on

Vue - defineModel

Authors
  • avatar
    Name
    Deng Hua
    Twitter

随着 vue3.4 版本的发布, defineModel 也将进入稳定期。它可以简化父子组件之间的双向绑定,是目前官方推荐的双向绑定方法。

Vue3.4 —— defineModel

目录

过去如何实现双向绑定

大家都应该知道 v-model 只是语法糖。它实际上定义了组件的 modelValue 属性并监听 update:modelValue 事件。

因此之前要手动实现双向数据绑定,需要你需要为子组件定义一个 modelValue 属性,当你想要更新子组件中的 modelValue 值时,你需要 emit 发送一个 update:modelValue 事件。将新值作为第二个参数传递。

让我们看一个简单的例子。父组件的代码如下:

<template>
  <CommonInput v-model="inputValue" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const inputValue = ref();
</script>

子组件的代码如下:

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup lang="ts">
const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
</script>

上面的例子是我们过去使用 v-model 实现双向绑定的方式。有一个问题,<input>明明支持直接使用 v-model ,但是我们这里没有使用 v-model ,而是添加了 value 属性和 input 事件到输入框。

原因是,从Vue2开始,单向的数据流就是官方推荐的开发范式,子组件中无法直接修改 props 中的值。

相反,应该从子组件抛出一个事件,父组件监听该事件,然后修改传递给父组件中的 props 的变量。如果我们这里直接在输入框中添加 v-model = 'props.modelValue’ ,其实就是直接在子组件的 props 中修改 modelValue 。由于单向数据流的原因,Vue 不支持直接修改 props ,所以我们需要按照上面的方式编写代码。

使用 defineModel 实现双向数据绑定

父组件的代码和之前一样,如下:

<template>
  <CommonInput v-model="inputValue" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const inputValue = ref();
</script>

子组件的代码如下:

<template>
  <input v-model="model" />
</template>

<script setup lang="ts">
const model = defineModel();
model.value = "xxx";
</script>

上面的例子中,我们直接使用 v-modeldefineModel 的返回值绑定到输入框,没有定义 modelValue 属性和监听 update:modelValue

我们可以修改子组件中 model 变量的值,父组件中 inputValue 变量的值也同步更新,这样也可以实现双向绑定。


现在问题来了,刚才不是说要使用单向数据流进行操作吗。这个例子中,当子组件的值修改时,父组件的值也会改变(model.value = 'xxx')。这不就又回到了Vue1的双向数据流了吗?

其实不然,它仍然是单向的数据流。下面简单解释一下defineModel的实现原理。

defineModel 实现原理

DefineModel 实际上在子组件中定义了一个名为model的变量作为 ref,将 modelValue 定义为 props,并且它还监听 props 中的 modelValue。当props中modelValue的值发生变化时,会同步更新model变量的值。

此外,当子组件内的model变量发生变化时,它会发出update:modelValue事件。一旦父组件接收到该事件,就会更新父组件内对应的变量值。

实现原理代码如下:

<template>
  <input v-model="model" />
</template>

<script setup lang="ts">
import { ref, watch } from "vue";

const model = ref();
const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);

// 父 -> 子
watch(
  () => props.modelValue,
  () => {
    model.value = props.modelValue;
  }
);

// 子 -> 父
watch(model, () => {
  emit("update:modelValue", model.value);
});
</script>

看完上面的代码,你应该明白为什么在子组件中可以直接修改 defineModel 的返回值,并且父组件对应的变量也会同步更新。

我们修改的实际上是 defineModel 返回的 ref 变量,而不是直接修改 props 中的 modelValue 。实现方法仍然和 vue3.4 之前的双向绑定相同,只不过 defineModel 宏帮助我们将之前繁琐的代码封装到内部实现中。

事实上, defineModel 的源代码是使用 customRefwatchSyncEffect 实现的。这里使用了 refwatch 的例子是为了让大家更容易理解 defineModel 的实现原理。

如何在 defineModel 中定义 type 、 default 等

由于 defineModel 声明了 prop ,因此它还可以定义 proptypedefault 。具体代码如下。

const model = defineModel({ type: String, default: "20" });

除了支持 typedefault 之外,还支持 requiredvalidator 。用法与定义 prop 时相同。

如何在 defineModel 中实现多个 v-model 绑定?

它还支持父组件上的多个 v-model 绑定。此时,我们传递给 defineModel 的第一个参数不是对象,而是字符串。

const model1 = defineModel("count1");
const model2 = defineModel("count2");

在父组件中使用 v-model 时的代码如下:

<CommonInput v-model:count1="inputValue1" />
<CommonInput v-model:count2="inputValue2" />

我们还可以在多个 v-model 中定义 typedefault

const model1 = defineModel("count1", {
  type: String,
  default: "aaa",
});

如何在 defineModel 中使用内置的自定义修饰符?

如果想使用系统内置的修饰符如 trim ,父组件的写法还是和之前一样:

<CommonInput v-model.trim="inputValue" />

子组件无需做任何修改,与上面其他 defineModel 示例相同:

const model = defineModel();

defineModel 还支持自定义修饰符。例如,如果我们想实现一个 uppercase 自定义修饰符,将输入框中的所有字母更改为大写,那么我们还需要使用内置的 trim 修饰符。

父组件代码如下:

<CommonInput v-model.trim.uppercase="inputValue" />

子组件需要这样写:

<template>
  <input v-model="modelValue" />
</template>

<script setup lang="ts">
const [modelValue, modelModifiers] = defineModel({
  set(value) {
    if (modelModifiers.uppercase) {
      return value?.toUpperCase();
    }
  },
});
</script>

此时,我们传递给 defineModel 的第一个参数是一个包含 getset 方法的对象。当读取 modelValue 变量时,它会进入 get 方法。当写入 modelValue 变量时,它将进入 set 方法。如果只需要拦截写操作,则可以省略 get

defineModel 的返回值也可以被解构为两个变量。第一个变量是我们在前面的示例中用于 v-model 绑定的 ref 对象。第二个变量是一个对象,其中包含修饰符。这里我们有两个修饰符trimuppercase ,所以 modelModifiers 的值为:

{
  trim: true,
  uppercase: true
}

当在输入框中输入内容时,会转到 set 方法,然后调用 value?.toUpperCase() 可以将输入的字母转换为大写。

End.