1. 问题描述
在前端开发的过程中碰到了这样的一个问题,首先有一个方法是负责去查字典,如果查询到了就把该信息缓存在前端,然后用的时候就先判断缓存有没有数据,如果有就直接读取,如果没有再向后端查询。在这个过程中发生了误操作缓存到前端的数据,导致其他地方使用的时候出现了错误
2. 案例分析
2.1 代码还原
用一段代码来描述这个问题
js
<template>
<div>
<button @click="handleTest">测试</button>
</div>
</template>
<script lang="ts" setup>
import {ref} from "vue";
// 模拟调用后端获取到缓存在前端的的数据
const cacheDictData = [{"value": "1"}, {"value": "2"}, {"value": "3"}]
const handleTest = () => {
const dicts = ref(cacheDictData)
console.log('原始数据:')
console.log('----------cacheDictData----------')
console.table(cacheDictData)
console.log('----------dicts----------')
console.table(dicts.value)
dicts.value.forEach(item => item.value = Number(item.value))
dicts.value = dicts.value.filter(item => item.value !== 2)
console.log('误操作之后的数据:')
console.log('----------cacheDictData----------')
console.table(cacheDictData)
console.log('----------dicts----------')
console.table(dicts.value)
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="less">
</style>
2.2 功能描述
页面很简单,就一个按钮
这段代码的作用本意很简单,在页面上新创建一个变量dicts
,用来存储从缓存中获取到的数据。然后需要先把拿到的数据的value
转换成Number
类型,然后再进行过滤,最终拿到结果。在这个过程中,页面上定义的dicts
确实是按照预期的获取到数据并且过滤了。但是因为其他地方也是使用相同的缓存数据cacheDictData
,这里操作数据的方式不对,导致了其他地方出现了异常。
现在点击按钮之后观察数据的变化: 可以看到dicts
是预期之内的结果,但是cacheDictData
的数据类型,居然也变成了Number
2.3 原因分析
分析过程也是坎坷曲折,一开始想的是既然操作的都是dicts
,如果数据类型变了,那么按道理来说value
为2的那项数据应该也会在cacheDictData
中被一起删除。就是因为这个导致思考了很久。后面发现是因为dicts
是在页面中新建的,也就是说dicts.value = dicts.value.filter(item => item.value !== 2)
这个操作,实际是对dicts
的引用起作用,但是操作dicts.value.forEach(item => item.value = Number(item.value))
的时候,操作的是cacheDictData
里面每一项的引用,从而导致了不知不觉中把cacheDictData
的数据类型给改了,导致问题。
3. 后端验证
分析了这个问题的原因,肯定要对这个问题进行一个确认,前端看不了数组和对象的引用,那么就换一下,用java
去模拟这个过程,看看操作的对象,对上述分析过程进行验证
Dict
类
java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Dict {
private String dict;
private String value;
}
测试类
java
public class Test {
public static void main(String[] args) {
List<Dict> cacheDictData = new ArrayList<>();
cacheDictData.add(new Dict("value", "1"));
cacheDictData.add(new Dict("value", "2"));
cacheDictData.add(new Dict("value", "3"));
List<Dict> dicts = cacheDictData;
System.out.println("操作之前");
System.out.println("dicts = " + dicts);
System.out.println("cacheDictData = " + cacheDictData);
dicts.forEach(item -> item.setValue(item.getValue() + "操作之后"));
dicts = dicts.stream().filter(dict -> !"2操作之后".equals(dict.getValue())).collect(Collectors.toList());
System.out.println("操作之后");
System.out.println("dicts = " + dicts);
System.out.println("cacheDictData = " + cacheDictData);
}
}
查看结果,可以看到dicts
集合的元素变少了,并且value
也变了,同时cacheDictData
的值也变了
java
操作之前
dicts = [Dict(dict=value, value=1), Dict(dict=value, value=2), Dict(dict=value, value=3)]
cacheDictData = [Dict(dict=value, value=1), Dict(dict=value, value=2), Dict(dict=value, value=3)]
操作之后
dicts = [Dict(dict=value, value=1操作之后), Dict(dict=value, value=3操作之后)]
cacheDictData = [Dict(dict=value, value=1操作之后), Dict(dict=value, value=2操作之后), Dict(dict=value, value=3操作之后)]
用debug模式,查看dicts
和cacheDictData
里每个元素的地址是否一样即可验证: 可以看到cacheDictData
是507,里面的三个元素是511、512、513
再查看dicts
,可以看到一开始的时候跟cacheDictData
是一样的
但是为什么会结果不一样呢,问题就出在filter
这个操作,这个操作会生成一个新的集合,然后这时候再赋值给dicts
,dicts
就发生了改变,可以往下再debug验证。等执行完filter
之后再查看dicts
,发现dicts
的地址已经发生了改变,但是里面每一个元素的地址还是不变的,这跟结果也刚好符合
4. 解决
其实这个解决的方法也很简单,可以通过深拷贝来获得一个新的对象即可 例如我这里把直接获取改为通过JSON.parse(JSON.stringify(cacheDictData))
来获取一个新的对象
JS
// const dicts = ref(cacheDictData)
const dicts = ref(JSON.parse(JSON.stringify(cacheDictData)))
这样就不会影响原有数据
当然前端也可以使用其他库,比如lodash
,VueUse
等,我这里就不过多赘述
5. 总结
经过解决这次的问题,无论是js
还是java
,在操作对象的时候需要考虑对象引用的传递,需要考虑清楚操作的是新的对象还是原有对象,防止此类问题的发生