Vue3 子组件修改父组件传递的对象并同步的方法汇总

Vue3 子组件修改父组件传递的对象并同步(最佳实践)

要实现子组件修改父组件传递的对象并同步,核心遵循 Vue 「单向数据流」原则:子组件不直接修改 Props 对象,而是通过「自定义事件传递新值 → 父组件接收并更新源数据」(对象是引用类型,直接修改 Props 虽能同步,但会触发警告且数据溯源困难,属于错误用法)。以下是我收集的3 种主流实现方案

一、基础方案:自定义事件(通用 / 推荐)

这是最通用的方案,适配所有修改场景(单属性 / 多属性 / 嵌套对象),核心逻辑:

  1. 子组件基于原 Props 对象创建「新对象」(避免直接修改 Props);
  2. 子组件通过 emit 触发自定义事件,传递修改后的新对象 / 新属性;
  3. 父组件监听事件,接收新值并更新源对象。

完整示例

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:namev-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 跨组件 / 复杂状态管理 无需手动传递事件,自动同步
相关推荐
spionbo1 小时前
Vue3 前端分页功能实现的技术方案及应用实例解析
前端
Zyx20071 小时前
JavaScript 作用域与闭包(下):闭包如何让变量“长生不老”
javascript
AI绘画小331 小时前
Web 安全核心真相:别太相信任何人!40 个漏洞挖掘实战清单,直接套用!
前端·数据库·测试工具·安全·web安全·网络安全·黑客
7***n751 小时前
前端设计模式详解
前端·设计模式·状态模式
u***j3241 小时前
JavaScript在Node.js中的进程管理
开发语言·javascript·node.js
用户47949283569152 小时前
Vite 中 SVG 404 的幕后黑手:你真的懂静态资源处理吗?
前端·vite
用户47949283569152 小时前
javascript新进展你关注了吗:TC39 东京会议带来五大新特性
javascript
未来之窗软件服务2 小时前
幽冥大陆(三十五)S18酒店门锁SDK go语言——东方仙盟筑基期
java·前端·golang·智能门锁·仙盟创梦ide·东方仙盟·东方仙盟sdk
卸任2 小时前
解密Flex布局:为什么flex:1仍会导致内容溢出
前端·css·flexbox