- 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.