Vue3 子组件修改父组件传递的对象并同步(最佳实践)
要实现子组件修改父组件传递的对象并同步,核心遵循 Vue 「单向数据流」原则:子组件不直接修改 Props 对象,而是通过「自定义事件传递新值 → 父组件接收并更新源数据」(对象是引用类型,直接修改 Props 虽能同步,但会触发警告且数据溯源困难,属于错误用法)。以下是我收集的3 种主流实现方案
一、基础方案:自定义事件(通用 / 推荐)
这是最通用的方案,适配所有修改场景(单属性 / 多属性 / 嵌套对象),核心逻辑:
- 子组件基于原 Props 对象创建「新对象」(避免直接修改 Props);
- 子组件通过
emit触发自定义事件,传递修改后的新对象 / 新属性; - 父组件监听事件,接收新值并更新源对象。
完整示例
1. 子组件(Child.vue):触发事件传递新值
<template>
<div>
<p>当前用户:{{ user.name }}({{ user.age }}岁)</p>
<!-- 修改单个属性 -->
<button @click="updateAge">修改年龄为30岁</button>
<!-- 修改多个属性 -->
<button @click="updateUserInfo">完整更新用户信息</button>
<!-- 修改嵌套对象属性 -->
<button @click="updateNestedProp">修改地址</button>
</div>
</template>
<script setup>
// 1. 声明接收父组件的对象 Props
const props = defineProps({
user: {
type: Object,
required: true,
default: () => ({ name: '默认用户', age: 18, info: { address: '默认地址' } })
}
});
// 2. 声明自定义事件(通知父组件修改)
const emit = defineEmits(['update-user']); // 统一事件名,简化监听
// 场景1:修改单个属性
const updateAge = () => {
// 基于原对象创建新对象(保留其他属性,仅更新age)
const newUser = { ...props.user, age: 30 };
emit('update-user', newUser); // 传递新对象给父组件
};
// 场景2:修改多个属性
const updateUserInfo = () => {
const newUser = {
...props.user,
name: '李四',
age: 28,
hobby: '打球' // 新增属性
};
emit('update-user', newUser);
};
// 场景3:修改嵌套对象属性(关键:嵌套对象也要浅拷贝,避免引用共享)
const updateNestedProp = () => {
const newUser = {
...props.user,
info: { ...props.user.info, address: '北京市朝阳区' } // 拷贝嵌套对象
};
emit('update-user', newUser);
};
</script>
2. 父组件(Parent.vue):监听事件更新源数据
<template>
<div>
<h3>父组件源数据:{{ parentUser.name }} - {{ parentUser.age }}岁 - {{ parentUser.info.address }}</h3>
<!-- 监听子组件的自定义事件 -->
<Child :user="parentUser" @update-user="handleUpdateUser" />
</div>
</template>
<script setup>
import { reactive } from 'vue';
import Child from './Child.vue';
// 父组件的源对象(引用类型用reactive定义)
const parentUser = reactive({
name: '张三',
age: 25,
info: { address: '上海市浦东新区' }
});
// 接收子组件传递的新对象,更新源数据
const handleUpdateUser = (newUser) => {
// 方式1:全量覆盖(推荐,简单高效)
Object.assign(parentUser, newUser);
// 方式2:按需更新(适合仅需更新部分属性的场景)
// parentUser.age = newUser.age;
// if (newUser.name) parentUser.name = newUser.name;
};
</script>
二、简化方案:自定义 v-model(表单 / 双向绑定场景)
Vue3 支持自定义组件的 v-model,本质是「Props + update:xxx 事件」的语法糖,适合表单类双向绑定场景,写法更简洁。
完整示例
1. 子组件(Child.vue):适配 v-model 规范
<template>
<!-- 表单输入框双向绑定 -->
<div>
<input
type="text"
v-model="localName"
placeholder="修改姓名"
/>
<input
type="number"
v-model="localAge"
placeholder="修改年龄"
/>
<input
type="text"
v-model="localAddress"
placeholder="修改地址"
/>
</div>
</template>
<script setup>
import { computed } from 'vue';
// 1. 声明 v-model 对应的 Props(默认是 modelValue)
const props = defineProps({
modelValue: {
type: Object,
required: true
}
});
// 2. 声明 v-model 对应的更新事件(update:modelValue)
const emit = defineEmits(['update:modelValue']);
// 3. 用计算属性实现「获取值 + 触发更新」
// 姓名
const localName = computed({
get: () => props.modelValue.name,
set: (newVal) => {
emit('update:modelValue', { ...props.modelValue, name: newVal });
}
});
// 年龄
const localAge = computed({
get: () => props.modelValue.age,
set: (newVal) => {
emit('update:modelValue', { ...props.modelValue, age: newVal });
}
});
// 嵌套属性(地址)
const localAddress = computed({
get: () => props.modelValue.info.address,
set: (newVal) => {
emit('update:modelValue', {
...props.modelValue,
info: { ...props.modelValue.info, address: newVal }
});
}
});
</script>
2. 父组件(Parent.vue):直接用 v-model 绑定
<template>
<div>
<h3>v-model 双向绑定:{{ parentUser.name }} - {{ parentUser.age }}岁 - {{ parentUser.info.address }}</h3>
<!-- 无需手动监听事件,v-model 自动处理 -->
<Child v-model="parentUser" />
</div>
</template>
<script setup>
import { reactive } from 'vue';
import Child from './Child.vue';
const parentUser = reactive({
name: '张三',
age: 25,
info: { address: '上海市浦东新区' }
});
</script>
进阶:多 v-model 绑定
若需单独绑定对象的多个属性,可自定义 v-model 名称(如 v-model:name、v-model:age):
<!-- 子组件 -->
<script setup>
const props = defineProps(['name', 'age']);
const emit = defineEmits(['update:name', 'update:age']);
// 姓名 v-model
const localName = computed({
get: () => props.name,
set: (val) => emit('update:name', val)
});
// 年龄 v-model
const localAge = computed({
get: () => props.age,
set: (val) => emit('update:age', val)
});
</script>
<!-- 父组件 -->
<Child v-model:name="parentUser.name" v-model:age="parentUser.age" />
三、特殊场景:Pinia/Vuex(跨组件 / 复杂状态)
若父组件和子组件层级较深,或多个组件共享该对象,建议用 Pinia(Vue3 推荐)/ Vuex 管理状态,子组件直接修改全局状态,自动同步到所有引用的组件。
示例(Pinia)
1. 定义 Store(stores/user.js)
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: { name: '张三', age: 25, info: { address: '上海' } }
}),
actions: {
// 定义修改方法
updateUserInfo(newUser) {
this.userInfo = { ...this.userInfo, ...newUser };
}
}
});
2. 子组件直接修改 Store(无需事件传递)
<!-- Child.vue -->
<script setup>
import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
// 直接调用 Store 的方法修改,父组件自动同步
const updateAge = () => {
userStore.updateUserInfo({ age: 30 });
};
</script>
3. 父组件引用 Store 状态
<!-- Parent.vue -->
<script setup>
import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
// 直接使用 userStore.userInfo,子组件修改后自动同步
</script>
四、避坑核心要点
1. 绝对禁止直接修改 Props 对象
以下写法虽能同步,但违背单向数据流,Vue 会抛出警告,且数据变更难以溯源:
// 子组件错误写法!!!
const badUpdate = () => {
props.user.age = 30; // 直接修改 Props 对象的属性
};
2. 嵌套对象必须浅拷贝
修改嵌套对象时,需对嵌套层级单独拷贝(否则仍会共享引用):
// 错误:仅拷贝外层,嵌套对象仍共享引用
const newUser = { ...props.user };
newUser.info.address = '北京'; // 仍会直接修改原 Props
// 正确:拷贝嵌套对象
const newUser = {
...props.user,
info: { ...props.user.info, address: '北京' }
};
3. 深拷贝的使用场景
若对象层级极深(如 3 层以上),可使用深拷贝(简单场景用 JSON.parse(JSON.stringify()),复杂场景用 lodash.clonedeep):
// 深拷贝(注意:无法拷贝函数、Symbol、undefined)
const newUser = JSON.parse(JSON.stringify(props.user));
newUser.info.detail.address = '北京';
emit('update-user', newUser);
4. 性能优化
频繁修改对象时,可通过「传递修改项而非完整对象」减少数据拷贝:
// 子组件:仅传递修改的属性
emit('update-user-prop', { key: 'age', value: 30 });
// 父组件:按需更新
const handleUpdateProp = ({ key, value }) => {
parentUser[key] = value;
};
五、总结
| 方案 | 适用场景 | 核心优点 |
|---|---|---|
| 自定义事件 | 通用场景(单 / 多属性、嵌套对象) | 符合单向数据流,数据可溯源 |
| 自定义 v-model | 表单类双向绑定场景 | 语法简洁,贴近原生 v-model |
| Pinia/Vuex | 跨组件 / 复杂状态管理 | 无需手动传递事件,自动同步 |