vue computed拦截v-model

316 阅读2分钟

最近维护代码发现一个很恶心的东西,旧代码使用组件没有遵循单向数据流,直接修改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>