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 跨组件 / 复杂状态管理 无需手动传递事件,自动同步
相关推荐
仰望.几秒前
vue 甘特图 vxe-gantt table 依赖线的使用,配置连接线
vue.js·甘特图
小时前端1 分钟前
谁说 AI 历史会话必须存后端?IndexedDB方案完美翻盘
前端·agent·indexeddb
wordbaby5 分钟前
TanStack Router 基于文件的路由
前端
wordbaby10 分钟前
TanStack Router 路由概念
前端
wordbaby13 分钟前
TanStack Router 路由匹配
前端
cc蒲公英13 分钟前
vue nextTick和setTimeout区别
前端·javascript·vue.js
程序员刘禹锡18 分钟前
Html中常用的块标签!!!12.16日
前端·html
sinat_3842410924 分钟前
OpenSpeedy 是一款开源免费的游戏变速工具
javascript
我血条子呢28 分钟前
【CSS】类似渐变色弯曲border
前端·css
DanyHope29 分钟前
LeetCode 两数之和:从 O (n²) 到 O (n),空间换时间的经典实践
前端·javascript·算法·leetcode·职场和发展