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