Vue3 子组件修改Props数组,父组件数据同步?问题分析及解决
2025-02-01 02:53:29
子组件修改Props数组导致父组件数据同步
当在Vue.js 3中开发时,有时会遇到一种棘手的情况:子组件接收来自父组件的数组Props,并且在子组件中对其进行修改时,父组件的原始数组也受到了影响。这种现象会让人感到困惑,也容易导致难以调试的问题。 本文分析产生该问题的原因并给出对应的解决方案。
问题分析:引用传递
问题的根源在于JavaScript的对象和数组是通过引用传递的。这意味着,当一个对象或数组被赋值给另一个变量时,两个变量都指向内存中的同一个数据。如果其中一个变量修改了数据,另一个变量也会看到相应的变化。
在上述代码中,App.vue
将locations
数组作为Props传递给UserLocations.vue
。 子组件的 locations
变量通过 [...this.initalLocations]
创建了一个浅拷贝。然而,该浅拷贝仅仅拷贝了数组的外层结构 ,并没有对数组中存储的对象进行深拷贝 。因此 locations
中存储的对象与 父组件 locations
中存储的对象指向相同的内存地址。当子组件内部修改对象属性时,实际上直接修改了父组件数据中的对象。
简单来讲,子组件并没有“复制”props里的数组,而仅仅只是复制了引用,所以导致两边数据联动。
解决方案一:深拷贝
最直接的办法是在子组件内部创建一个数组的深拷贝。通过深拷贝,子组件拥有了一个独立于父组件的全新数组,任何修改都不会影响到父组件的数据。深拷贝可以解决这种引用带来的数据同步问题。
代码示例:
使用 JSON.parse(JSON.stringify(this.initialLocations))
创建深拷贝:
<template>
<ul>
<li v-for="(location) in locations"
:key="location.id"
>
<span @click="decreaseOrder(location)">Up</span>
{{ location.name}} {{location.order}}
<span @click="increaseOrder(location)">down</span>
</li>
</ul>
</template>
<script>
export default {
data: function() {
return {
locations: []
}
},
props: {
initalLocations: {
type: Array,
},
},
watch: {
initalLocations: {
handler(newVal) {
this.locations = JSON.parse(JSON.stringify(newVal));
},
immediate: true
}
},
methods:{
increaseOrder(location) {
if (location.order != this.locations.length) {
this.locations = this.locations.map(l => {
var returnLocation = {...l};
if (l.id == location.id) {
returnLocation.order += 1;
}
return returnLocation
});
}
},
decreaseOrder(location) {
if (location.order != 1) {
this.locations = this.locations.map(l => {
var returnLocation = {...l};
if (l.id == location.id) {
returnLocation.order -= 1;
}
return returnLocation
});
}
},
}
}
</script>
操作步骤:
- 将子组件中
data
属性下的locations
初始化为空数组。 - 添加
watch
侦听器,当initalLocations
props 值发生更改时, 将新 props 深拷贝到locations
中,
使用JSON.parse(JSON.stringify(newVal))
创建newVal
的深拷贝。immediate:true
保证首次渲染组件,也能获得数据拷贝 - 修改
methods
中更改顺序的逻辑,确保所有逻辑操作,仅修改子组件本地的locations
数据。
原理:
JSON.stringify()
方法将 JavaScript 对象转换为 JSON 字符串, JSON.parse()
方法将 JSON 字符串解析为 JavaScript 对象。 当一个对象被序列化再反序列化时,会创建一个全新的对象。因此可以有效的创建数据深拷贝,并保证数组中的每个对象也完成深拷贝。
安全建议: 这种方法对于简单对象和数组非常方便,但对于包含循环引用或者函数等特殊值的对象可能导致问题,不适合复杂对象深拷贝。
解决方案二:使用计算属性
计算属性可以创建一个基于响应式数据的新的值,它的优点在于具有缓存性,只有在相关依赖发生变化时才重新计算,能有效地避免不必要的性能损耗。通过将 initalLocations
props 定义为计算属性的返回值,每次组件更新时, 可以实现创建一个新的拷贝,保证了子组件不会直接操作父组件的数据。
代码示例:
<template>
<ul>
<li v-for="(location) in locations"
:key="location.id"
>
<span @click="decreaseOrder(location)">Up</span>
{{ location.name}} {{location.order}}
<span @click="increaseOrder(location)">down</span>
</li>
</ul>
</template>
<script>
export default {
props: {
initalLocations: {
type: Array,
},
},
computed: {
locations() {
return this.initalLocations.map(location => ({...location}));
}
},
methods:{
increaseOrder(location) {
if (location.order != this.locations.length) {
this.$emit("update-location-order",location, location.order+1)
}
},
decreaseOrder(location) {
if (location.order != 1) {
this.$emit("update-location-order",location, location.order-1)
}
}
}
}
</script>
<template>
<user-locations :initalLocations="locations" @update-location-order="updateOrder"/>
</template>
<script>
import UserLocations from './components/UserLocations.vue'
export default {
name: 'App',
components: {
UserLocations
},
data: function() {
return {
locations: [
{"id": 121, name: "test 121", "order": 1},
{"id": 122, name: "test 122", "order": 2},
{"id": 123, name: "test 123", "order": 3},
{"id": 124, name: "test 124", "order": 4}
]
}
},
methods: {
updateOrder(location, order){
const index = this.locations.findIndex(l=> l.id == location.id)
this.locations[index].order = order;
}
}
}
</script>
操作步骤:
- 移除
data
选项中的locations
, 定义一个名为locations
的计算属性,利用map
函数遍历 propsinitalLocations
数组, 创建并返回一个新数组, 新数组的每一项,通过{...location}
创建对象的浅拷贝。 - 通过子组件通过
$emit
发射自定义事件 "update-location-order",并向父组件传递更改后的location
以及新的order
值,实现子组件对 props 数组的操作逻辑外置到父组件。 - 在父组件中,通过监听
"update-location-order"
事件, 更新对应 location 对象的order
。
原理:
使用计算属性保证子组件 locations
属性总是获得 props 数据新拷贝。 使用 this.$emit
和自定义事件的结合方式将 props数据的更改权限传递回父组件, 这种操作数据方式通常用于更严格的父子数据传递模式中, 子组件只应该发射修改事件, 负责数据的操作逻辑应该外置。
安全建议: 在使用这种方式操作数组的时候, 仍旧应该避免直接操作传递给子组件的数据的每一项(每一项数据还是通过浅拷贝过来的,深层嵌套的数据仍存在引用问题) ,在methods
中需要确保不产生副作用。
结论
理解JavaScript引用传递的概念,并且在需要时使用正确的复制手段对确保Vue 组件间数据的正确性和稳定性非常重要。两种解决方案各有侧重,可根据项目场景灵活使用。 使用深拷贝更加便捷,而使用计算属性配合自定义事件操作props,更加安全和利于管理。开发者可以根据项目需求灵活选择,实现更可维护的 Vue 应用程序。