子组件修改props对象属性会直接改变父组件数据,因vue默认浅拷贝导致引用共享;正确做法是在子组件初始化时用json.parse(json.stringify())或lodash.clonedeep深拷贝,或对大数据量采用不可变更新。

因为 emit 传的是引用,不是副本 —— 子组件拿到的仍是父组件对象在内存中的同一份地址。你改它的属性,等于直接改父组件原始数据,根本不需要 emit 就已同步生效。这不是“改不动”,而是“改得太快、太直接”,违背了 Vue 单向数据流的设计初衷。
问题本质:浅拷贝陷阱
Vue 的 props 传递复杂类型(Object / Array)时,默认只做浅拷贝:只复制外层数组或对象的引用,内部嵌套的对象仍共享内存地址。所以:
- 子组件修改
props.obj.name = 'new'→ 父组件对应对象的name立刻变 - 子组件调用
this.$emit('update', this.obj)→ 实际传的还是那个被改过的原对象 - 父组件监听到事件后赋值:
this.data = e→ 相当于又把改过的对象重新赋给自己,毫无隔离
正确做法:在子组件内切断引用链
必须在子组件初始化阶段,就对 props 数据做独立副本处理,后续所有编辑都基于这个副本进行:
- 小数据量 + 无特殊类型(无 function/undefined/symbol):用 JSON 方案最简
-
中大型对象或含函数、Date、RegExp 等:用
lodash/cloneDeep
const localData = ref(cloneDeep(props.originalObj))
- 大数据量表格类场景(如 list 数百项):避免深拷贝开销,改用“不可变更新”
// 修改某一项时不改原对象,而是生成新对象替换
const updateItem = (index, updates) => {
localList.value = localList.value.map((item, i) =>
i === index ? { ...item, ...updates } : item
)
}
别踩这些常见坑
- 用 Object.assign({}, props.obj) → 只是第一层浅拷贝,嵌套对象依然共享
- 用 ref(props.obj) → 完全没拷贝,还是同一引用
-
computed(() => props.obj) → 响应式读取没问题,但一旦赋值(
xxx.value = ...)就会污染父组件 - 在 emit 前才深拷贝 → 此时属性早已被改过,深拷贝只是把“脏数据”再传一遍
父组件配合要点
父组件不该被动接收“可能已被改”的对象,而应主动设计为只响应明确提交的动作:
- 子组件点击“确定”才 emit;“取消”不 emit,也不保留任何中间状态
- 父组件收到 emit 后,用新数据 整体替换 原始数据(而非 merge 或局部赋值),保证状态清晰可追溯
- 若需回显编辑态,子组件内部维护 localData,父组件只负责传初始值和接收最终结果










