在子组件修改父组件传递过来的v-model,这样会破坏单向数据流,造成屎山代码,为了避免这个问题,需要给一个中间层来相对舒服的使用v-model。方法就是用computed去拦截v-model,然后在computed 里面去触发 emit 事件来修改父组件传来的v-model
以下是Vue 3.4 之前版本的做法
核心实现逻辑
- 子组件接收
v-model
默认绑定modelValue
属性,通过defineProps
接收父组件传递的值。 - 定义
emit
事件
使用defineEmits
声明update:modelValue
事件用于更新父组件数据。 - 通过
computed
拦截
使用computed
的get/set
方法,在set
中触发emit
回传数据。
二、基础示例代码
父组件
<template>
<ChildComponent v-model="message" />
</template>
<script setup>
import { ref } from 'vue';
const message = ref('Hello');
</script>
子组件
<template>
<input v-model="proxyValue" />
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
// 核心:通过 computed 拦截
const proxyValue = computed({
get: () => props.modelValue,
set: (newValue) => {
emit('update:modelValue', newValue); // 触发父组件更新
}
});
</script>
三、处理对象类型 v-model
若父组件传递对象,需为每个属性单独拦截:
父组件
<template>
<ChildComponent v-model="form" />
</template>
<script setup>
import { reactive } from 'vue';
const form = reactive({ name: 'Alice', age: 25 });
</script>
子组件
<template>
<input v-model="nameProxy" />
<input v-model="ageProxy" />
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({ modelValue: Object });
const emit = defineEmits(['update:modelValue']);
// 拦截对象属性
const nameProxy = computed({
get: () => props.modelValue.name,
set: (val) => {
emit('update:modelValue', { ...props.modelValue, name: val });
}
});
const ageProxy = computed({
get: () => props.modelValue.age,
set: (val) => {
emit('update:modelValue', { ...props.modelValue, age: val });
}
});
</script>
以下是 Vue 3.4 的做法
在 Vue3.4 及以上版本中,官方新增的 defineModel
宏可以大幅简化 v-model
的双向绑定逻辑,完全替代之前基于 computed
+ emit
的拦截方案。以下是改写方法和对比分析:
一、用 defineModel
改写原方案的核心优势
- 代码简化
无需手动声明props
和emit
,defineModel
自动处理modelValue
和update:modelValue
的逻辑12。 - 原生支持响应式
直接返回一个ref
对象,无需通过computed
的get/set
手动触发更新。 - 兼容修饰符
支持v-model
的内置修饰符(如.trim
)和自定义修饰符,可自动处理值转换逻辑。
二、基础示例对比
新方案(基于 defineModel
)
<!-- 子组件 -->
<script setup>
const model = defineModel(); // 自动处理 prop 和 emit
</script>
<template>
<input v-model="model" />
</template>
三、进阶用法
1. 多 v-model
绑定
父组件:
<ChildComponent v-model:name="name" v-model:age="age" />
子组件:
<script setup>
const nameModel = defineModel('name');
const ageModel = defineModel('age');
</script>
2. 类型校验与默认值
<script setup>
const model = defineModel({
type: String,
default: '默认值'
});
</script>
3. 自定义修饰符
父组件:
<ChildComponent v-model.uppercase="text" />
子组件:
<script setup>
const [model, modifiers] = defineModel({
set(value) {
return modifiers.uppercase ? value.toUpperCase() : value;
}
});
</script>
结论: 可以看到在 vue 3.4 以后,代码量大幅减少,所以能升级就升级吧!