# v-model实现原理
单项数据流我们知道子组件是不能去修改父组件传递过来的数据,如果想要修改,只能通过事件回调的方式,但是这样子组件就失去了数据驱动的能力,所以vue3.0中引入了v-model指令,它实现了数据的双向绑定,那么它是如何实现的呢?
<!-- 父组件 -->
<template>
<child v-model="number"></child>
<!-- 等同于 -->
<child :modelValue="number" @update:modelValue="number = $event"></child>
</template>
<script setup>
import { ref } from 'vue'
import Child from './child.vue'
const number = ref(0)
</script>
<!-- 子组件 -->
<template>
<div>
<input type="number" :modelValue="modelValue" @update:modelValue="onValueChange" />
</div>
</template>
<script setup>
const props = defineProps({
modelValue: {
type: Number,
default: 0
}
})
const emit = defineEmits(['update:modelValue'])
function onValueChange(e) {
emit('update:modelValue', e.target.value)
}
</script>
这也是普通封装组件的方法,下面我们就利用计算属性实现v-model
# computed拦截prop
<!-- 父组件 -->
...省略其他...
<child v-model="number"></child>
...省略其他...
<!-- 子组件 -->
<template>
<div>
<input type="number" v-model="num" />
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: {
type: Number,
default: 0
}
})
const emit = defineEmits(['update:modelValue'])
const num = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
# 绑定对象
那如果当v-model绑定的是对象呢?则可以利用computed拦截多个值。
<!-- 父组件 -->
<template>
<child v-model="person"></child>
</template>
<script setup>
const person = ref({
name: '张三',
age: 18
})
</script>
<!-- 子组件 -->
<template>
<div>
<input v-model="name" />
<input v-model="age" />
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue'])
const name = computed({
get() {
return props.modelValue.name
},
set(value) {
emit('update:modelValue', { ...props.modelValue, name: value })
}
})
const age = computed({
get() {
return props.modelValue.age
},
set(value) {
emit('update:modelValue', { ...props.modelValue, age: value })
}
})
</script>
当然,针对简单的对象我们可以这样,但是如果是一个复杂且属性很多的对象,代码冗余量就会增加很多,所以需要我们将拦截整合起来。
# 监听整个对象
<!-- 父组件 -->
<template>
<child v-model="person"></child>
</template>
<script setup>
const person = ref({
name: '张三',
age: 18
})
</script>
<!-- 子组件 -->
<template>
<div>
<input v-model="person.name" />
<input v-model="person.age" />
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue'])
const person = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
但是这种直接拦截整个对象,改变对象某个属性并不会生效,也就是不会触发 set,只有修改整个对象, person = xxx 才会触发 set。
# Proxy代理对象
<!-- 子组件 -->
<template>
<div>
<input v-model="person.name" />
<input v-model="person.age" />
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue'])
const person = computed({
get() {
return new Proxy(props.modelValue, {
get(target, key) {
return Reflect.get(target, key);
},
set(target, key, value,receiver) {
emit("update:modelValue", {
...target,
[key]: value,
});
return true;
},
});
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
为了方便使用,封装hook
import { computed } from "vue";
export default function useVModle(props, propName, emit) {
return computed({
get() {
return new Proxy(props[propName], {
get(target, key) {
return Reflect.get(target, key)
},
set(target, key, newValue) {
emit('update:' + propName, {
...target,
[key]: newValue
})
return true
}
})
},
set(value) {
emit('update:' + propName, value)
}
})
}
<!-- 子组件使用 -->
<script setup>
import useVModel from "../hooks/useVModel";
const props = defineProps({
modelValue: {
type: Object,
default: () => {},
},
});
const emit = defineEmits(["update:modelValue"]);
const form = useVModel(props, "modelValue", emit);
</script>