- Published on
Vue - defineModel
- Authors

- Name
- Deng Hua
随着 vue3.4 版本的发布, defineModel 也将进入稳定期。它可以简化父子组件之间的双向绑定,是目前官方推荐的双向绑定方法。
目录
- 过去如何实现双向绑定
- 使用 defineModel 实现双向数据绑定
- defineModel 实现原理
- 如何在 defineModel 中定义 type 、 default 等
- 如何在 defineModel 中实现多个 v-model 绑定?
- 如何在 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-model 将 defineModel 的返回值绑定到输入框,没有定义 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 的源代码是使用 customRef 和 watchSyncEffect 实现的。这里使用了 ref 和 watch 的例子是为了让大家更容易理解 defineModel 的实现原理。
如何在 defineModel 中定义 type 、 default 等
由于 defineModel 声明了 prop ,因此它还可以定义 prop 的 type 和 default 。具体代码如下。
const model = defineModel({ type: String, default: "20" });
除了支持 type 和 default 之外,还支持 required 和 validator 。用法与定义 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 中定义 type 、 default 等
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 的第一个参数是一个包含 get 和 set 方法的对象。当读取 modelValue 变量时,它会进入 get 方法。当写入 modelValue 变量时,它将进入 set 方法。如果只需要拦截写操作,则可以省略 get 。
defineModel 的返回值也可以被解构为两个变量。第一个变量是我们在前面的示例中用于 v-model 绑定的 ref 对象。第二个变量是一个对象,其中包含修饰符。这里我们有两个修饰符trim和uppercase ,所以 modelModifiers 的值为:
{
trim: true,
uppercase: true
}
当在输入框中输入内容时,会转到 set 方法,然后调用 value?.toUpperCase() 可以将输入的字母转换为大写。
End.