vue3 reactive + v-model 双向绑定

<template>
  <el-form>
    <el-form-item>
      <el-input v-model="filter.a" />
    </el-form-item>
    <el-form-item>
      <el-button @click="handleEmit">ok</el-button>
    </el-form-item>
  </el-form>
</template>

<script setup lang="ts">
import { reactive } from 'vue';

defineOptions({
  name: 'FilterForm',
});
const props = withDefaults(
  defineProps<{
    modelValue?: Record<string, string>;
  }>(),
  {
    modelValue: () => ({}),
  },
);
const emit = defineEmits<{
  (event: 'update:modelValue', modelValue: Record<string, string>): void;
}>();

// 根据传入值初始化
const filter = reactive({
  a: props.modelValue?.a ?? '',
});

// 提交
const handleEmit = () => {
  emit('update:modelValue', filter);
};
</script>

这个组件想要通过 v-model 双向绑定一个对象,点按钮的时候再更新 modelValue

问题出在调用上。我在调用时,直接传递了一个 reactive

<template>
  <filter-form v-model="filter" />
</template>

<script setup lang="ts">
import { reactive } from 'vue';

const filter = reactive({ a: '', b: '' });
</script>

我们都知道,vue2 选项式 API 的时候,直接通过 v-model 双向绑定一个对象不是什么大问题。但在 vue3 组合式 API,你无法直接修改一个响应式对象,即使这个响应式对象是用 let 声明的也不行。

// ×
filter = reactive({ a: 'a', b: 'b' });
// √
filter.a = 'a';
filter.b = 'b';

这部分原理在 vue3 官方文档里说得很清楚。摘录如下:

因为 Vue 的响应式系统是通过 property 访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地 “替换” 一个响应式对象。

emit 提交了一个新的对象,没有保持相同引用,所以无法触发响应式系统,表现出 v-model 失效了一样。解决方法还挺简单的,换成 ref 就行了。

<template>
  <filter-form v-model="filter" />
</template>

<script setup lang="ts">
import { ref } from 'vue';

const filter = ref({ a: '', b: '' });
</script>

更新时等同于修改 .value

filter.value = newFilter;

如果你正在烦恼 .value 的问题,你应该看看响应性语法糖 reactivity transform,它完美地解决了这个问题。

参考链接如下: