父组件向子组件传递对象类型参数时,绝对不能直接修改子组件接收到的 Props 对象(违背 Vue 单向数据流原则,会触发控制台警告,且数据溯源困难)。正确做法是:通过「子组件触发自定义事件 → 父组件接收事件并修改源数据」,或在子组件内创建对象副本修改(仅适用于无需同步父组件的场景)。
以下是两种核心场景的详细实现方案:
一、核心场景
对象是引用类型,子组件接收到的 Props 是父组件对象的「引用地址」:
- 直接修改
props.xxx.属性虽然能同步更新父组件数据,但属于「隐式修改」,Vue 强烈不推荐(维护性极差); - 所有对 Props 的修改,必须「由父组件主动完成」,子组件仅负责「通知父组件需要修改」并传递新值。
二、场景 1:修改后需同步到父组件(主流场景)
实现逻辑
- 子组件通过
defineEmits声明自定义事件; - 子组件触发事件时,传递「修改后的完整对象」或「具体修改项」;
- 父组件监听事件,接收新值并更新源对象。
完整示例
步骤 1:子组件(触发事件,传递新值)
<!-- Child.vue -->
<template>
<div>
<p>当前用户:{{ user.name }}({{ user.age }}岁)</p>
<!-- 按钮1:修改单个属性(传递修改项) -->
<button @click="updateAge">修改年龄为30岁</button>
<!-- 按钮2:修改多个属性(传递完整新对象) -->
<button @click="updateUser">完整修改用户信息</button>
</div>
</template>
<script setup>
// 1. 声明接收父组件的对象Props
const props = defineProps({
user: {
type: Object,
required: true,
default: () => ({ name: '默认', age: 18 })
}
});
// 2. 声明自定义事件(通知父组件修改)
const emit = defineEmits(['update-user', 'update-age']);
// 方式1:仅修改单个属性(传递具体值)
const updateAge = () => {
// 传递「新的年龄值」给父组件
emit('update-age', 30);
};
// 方式2:修改多个属性(传递完整新对象,推荐)
const updateUser = () => {
// 基于原Props对象创建新对象(避免直接修改Props)
const newUser = {
...props.user, // 拷贝原属性
name: '李四', // 覆盖新属性
age: 28,
hobby: '打球' // 新增属性
};
// 传递新对象给父组件
emit('update-user', newUser);
};
</script>
步骤 2:父组件(监听事件,更新源数据)
<!-- Parent.vue -->
<template>
<div>
<h3>父组件源数据:{{ parentUser.name }}({{ parentUser.age }}岁)</h3>
<!-- 监听子组件的自定义事件 -->
<Child
:user="parentUser"
@update-age="handleUpdateAge"
@update-user="handleUpdateUser"
/>
</div>
</template>
<script setup>
import { reactive } from 'vue';
import Child from './Child.vue';
// 父组件的源对象(用reactive定义,适合引用类型)
const parentUser = reactive({
name: '张三',
age: 25
});
// 处理子组件的「修改年龄」事件
const handleUpdateAge = (newAge) => {
// 父组件主动修改源数据
parentUser.age = newAge;
};
// 处理子组件的「完整修改用户」事件
const handleUpdateUser = (newUser) => {
// 方式1:直接替换(适合全量修改)
// Object.assign(parentUser, newUser);
// 方式2:按需覆盖(更灵活)
parentUser.name = newUser.name;
parentUser.age = newUser.age;
if (newUser.hobby) {
parentUser.hobby = newUser.hobby;
}
};
</script>
三、场景 2:修改后无需同步父组件(仅子组件内使用)
若子组件仅需基于父组件传递的对象做「本地修改」(不影响父组件),可在子组件内创建对象的「深拷贝副本」,修改副本而非原 Props。
实现示例
<!-- Child.vue -->
<template>
<div>
<p>父组件原始值:{{ user.name }}</p>
<p>子组件副本值:{{ localUser.name }}</p>
<button @click="modifyLocalUser">修改子组件本地副本</button>
</div>
</template>
<script setup>
import { ref, reactive, watch } from 'vue';
// 1. 接收父组件的对象Props
const props = defineProps(['user']);
// 2. 创建对象副本(深拷贝,避免引用关联)
// 方式1:浅拷贝(仅适用于单层对象)
// const localUser = reactive({ ...props.user });
// 方式2:深拷贝(推荐,支持嵌套对象)
const localUser = reactive(JSON.parse(JSON.stringify(props.user)));
// 可选:监听父组件Props变化,同步更新本地副本
watch(
() => props.user,
(newUser) => {
Object.assign(localUser, JSON.parse(JSON.stringify(newUser)));
},
{ deep: true } // 监听对象内部属性变化
);
// 3. 修改本地副本(不会影响父组件)
const modifyLocalUser = () => {
localUser.name = '子组件本地修改';
localUser.age = 99;
};
</script>
注意
- 浅拷贝(
{ ...props.user }/Object.assign)仅适用于单层对象,嵌套对象仍会共享引用; - 深拷贝推荐用
JSON.parse(JSON.stringify(props.user))(简单场景),复杂场景可使用lodash.clonedeep; - 若父组件后续更新了原对象,子组件的本地副本不会自动同步,需通过
watch监听 Props 并手动同步。
四、场景 3:双向绑定(v-model 简化写法)
Vue3 支持自定义组件的 v-model,可简化「Props + 事件」的双向绑定逻辑,本质仍是「子触发事件,父修改数据」的语法糖。
实现示例
子组件
<!-- Child.vue -->
<template>
<div>
<input
type="text"
v-model="localName"
placeholder="修改姓名"
/>
<input
type="number"
v-model="localAge"
placeholder="修改年龄"
/>
</div>
</template>
<script setup>
import { computed } from 'vue';
// 1. 声明Props(默认v-model对应modelValue)
const props = defineProps({
modelValue: {
type: Object,
required: true
}
});
// 2. 声明更新事件
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 });
}
});
</script>
父组件
<!-- Parent.vue -->
<template>
<div>
<p>双向绑定结果:{{ parentUser.name }}({{ parentUser.age }}岁)</p>
<!-- 直接用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 });
</script>
五、避坑重点
1. 禁止直接修改 Props 对象
以下写法虽然能生效,但属于错误用法(Vue 会警告):
// 子组件错误写法!!!
const modifyProps = () => {
props.user.age = 30; // 直接修改Props对象的属性
};
2. 深拷贝的注意事项
JSON.parse(JSON.stringify(obj))无法拷贝函数、Symbol、undefined 等类型,复杂场景需用lodash.clonedeep;- 若对象包含循环引用(如
obj.a = obj),JSON 深拷贝会报错,需手动处理。
3. 嵌套对象的修改
若传递的是嵌套对象(如 { user: { info: { age: 25 } } }),修改时仍需遵循「子传事件,父修改」:
// 子组件触发事件
emit('update-user', {
...props.user,
info: { ...props.user.info, age: 30 }
});
// 父组件接收
const handleUpdate = (newUser) => {
Object.assign(parentUser, newUser);
};
六、场景分析
| 场景 | 推荐方案 | 核心优点 |
|---|---|---|
| 修改后需同步父组件 | 子组件触发自定义事件 + 父组件修改源数据 | 符合单向数据流,数据可溯源 |
| 修改后无需同步父组件 | 子组件创建对象深拷贝副本,修改副本 | 不影响父组件,解耦性好 |
| 表单类双向绑定场景 | 自定义 v-model(Props + update 事件) | 语法简洁,符合 Vue 最佳实践 |
714

被折叠的 条评论
为什么被折叠?



