最近维护代码发现一个很恶心的东西,旧代码使用组件没有遵循单向数据流,直接修改props,造成了后续维护十分烧脑。vue3.4中推出了v-model,如何在vue3.4以前中使用组件优雅的遵循单项数据流,使用computed拦截v-model。
vue2
首先聊一下vue2的实现,vue2组件使用v-model,以下内容来自vue2官方文档。
自定义组件的 v-model
2.2.0+ 新增
一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的。model 选项可以用来避免这样的冲突:
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})
现在在这个组件上使用 v-model 的时候:
<base-checkbox v-model="lovingVue"></base-checkbox>
这里的 lovingVue 的值将会传入这个名为 checked 的 prop。同时当 <base-checkbox> 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的 property 将会被更新。
注意你仍然需要在组件的 props 选项里声明 checked 这个 prop。
所以我的实现通过computed作为中间层拦截修改并且$emit事件给父组件让父组件修改。至于为什么在computed的get方法内在使用Proxy代理是因为实现多层对象仍然可以拦截。
<template>
<div>
<input v-model="modelvalue.a" />
<van-search
v-model="modelvalue.b.x"
shape="round"
background="#fff"
placeholder="请输入搜索关键词"
/>
<van-datetime-picker
v-model="modelvalue.b.y"
type="date"
title="选择年月日"
:min-date="minDate"
:max-date="maxDate"
/>
</div>
</template>
<script>
export default {
props: {
value: {
type: Object,
default: null,
},
},
data() {
return {
minDate: new Date(2020, 0, 1),
maxDate: new Date(2025, 10, 1),
};
},
computed: {
modelvalue: {
get() {
const _that = this;
const proxy = new Proxy(this.value, {
get(target, key) {
return Reflect.get(target, key);
},
set(target, key, val) {
_that.$emit("input", {
...target,
[key]: val,
});
return true;
},
});
return proxy;
},
set(val) {
this.$emit("input", val);
},
},
},
};
</script>
vue3.x-3.4
vue3对组件的v-model与vue2有所不同,以下是vue3官方文档的描述,由于现在文档已经是最新的了,找不到之前的描述。
底层机制
defineModel 是一个便利宏。编译器将其展开为以下内容:
- 一个名为
modelValue的 prop,本地 ref 的值与其同步; - 一个名为
update:modelValue的事件,当本地 ref 的值发生变更时触发。
在 3.4 版本之前,你一般会按照如下的方式来实现上述相同的子组件:
vue
<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
然后,父组件中的 v-model="foo" 将被编译为:
template
<!-- Parent.vue -->
<Child
:modelValue="foo"
@update:modelValue="$event => (foo = $event)"
/>
于此了解vue3与vue2的差异在于传递到子组件的props为modelValue,子组件向父组件抛出事件应该$emit这个‘update:modelValue’,所以vue3的实现如下所示。
父组件
<script setup>
import { ref } from 'vue'
import Comp from './Comp.vue'
const obj = ref({
a: {
x: 2
},
B: 2
})
</script>
<template>
<input v-model="msg" />
<Comp v-model="obj"></Comp>
{{obj}}
</template>
子组件
<script setup>
import { onMounted,computed } from 'vue';
const props = defineProps({
modelValue: Object
})
const emit = defineEmits(['update:modelValue'])
const model = computed({
get() {
return new Proxy(props.modelValue, {
get(target, key) {
return Reflect.get(target, key)
},
set(target, key, val) {
emit('update:modelValue', { ...target, [key]: val })
return true
}
})
},
set(val) {
emit('update:modelValue', val )
}
})
</script>
<template>
<div>
<input v-model="model.B" />
<input v-model="model.a.x" />
</div>
</template>