bug 现象如图:
看上去像数据问题,但它的出现是偶现的,相同的条件,刷新页面后就恢复正常了。而且多出来的英文明显不是业务产生的乱码,像是代码里什么对象的方法属性。
根据以往的经验,所谓的偶现,大概率是还没有发现规律,但常见的接口响应时间,访问其他页面残留的全局设置等原因都试过了,还是没发现规律。
而且在本地开发过程中没遇到过,页面是从react 项目迁移到 vue的,业务逻辑没动,实现思路也是根据react 的实现思路写的,根据测试同学的反馈,说这种现象在react 项目时就出现过,不过概率比较低,在vue 重构后出现的概率好像是更高了。
我知道echarts 和 vue 的双向绑定实现代码存在冲突,不能把echarts 实例对象保存为vue 的响应式数据,否则会导致echarts 的 tooltip 失效。
所以 echarts 实例没有保存为响应式数据了,但是:
let timeObj = echarts.init(chartDom);
setTimeout(() => {
timeObj.hideLoading()
timeObj.setOption(option);
timeObj.resize()
item.echarExample= timeObj
}, 100);
中的 option 中依然是响应式数据,结合AI分析和其他技术同学沟通猜测可能是echarts 或特定版本的 echarts 和 vue ,react的数据绑定实现代码之间还有什么其他冲突,于是将接口数据的保存从响应式数据改为js 原生变量,并在timeObj.setOption(option);前添加option = JSON.parse(JSON.stringify(option));来去除不可遍历属性和原型链相关关联。
但这只是猜测,没有实际根据,没有绝对把握能解决这个问题,还有没有什么其他排查思路或实际经验?
ps:因为项目特点,换组件(vue-echarts) 的工作量和不确定性都比较高,所以暂时先不考虑换组件,看能不能在现有的基础上解决,或者是不是echarts 版本原因。
按照猜测思路修复过,且脱敏模糊后的核心代码和echarts 版本:
...
"echarts": "^5.5.0",
...
<template>
<div class="echartsbox">
<template v-for="item in data.vmmvEcharList" :key="item.id">
<div class="container">
<div :style="{
margin:'16px 16px 0px',
fontSize:'14px',
fontWeight:'500',
color:'#1D2129'
}">{{item.title}}</div>
<div v-if="item.echarData" :id="item.id" class="echart" >
<img :src="loadingImg" style="margin-top: 8px;width: 100%"/>
</div>
<div v-else class="echart container">
<el-empty description="暂无数据" :image-size="115" style="padding-top: 70px"/>
</div>
</div>
</template>
</div>
</template>
<script setup>
import {nextTick, onBeforeUnmount, onMounted, reactive} from "vue";
import loadingImg from './loading.jpg';
import Api from "@/api.js"
import * as echarts from 'echarts/core'
// 定义接收的 props
const props = defineProps({
// 表单数据
searchData: {
type: Object,
required: true
},
});
const echarList=[
{formatFun:
(data)=>{
const statusMap = new Map([["0", "待分析"], ["1", "待评审"], ["2", "已评审"], ["3", "退回分析"]]);
let changDate = {};
for (let i in data) {
if (statusMap.has(i)) {
const state = statusMap.get(i);
changDate[state] = data[i];
}
}
return changDate;
},colorArr:['#ff6284', '#00a8ff', '#00d274','#ff6284'],getDataFun:'fun1',echarExample:null, echarData:true,id:'lineEcharts3',title:'事故反馈数'},
{formatFun:(data)=>data,colorArr:['#ff6284', '#00a8ff', '#00d274'],getDataFun:'fun2',echarExample:null, echarData:true,id:'lineEcharts1',title:'责任判定分类'},
{formatFun:(data)=>data,colorArr:['#ff6284', '#ffd600', '#00a8ff', '#00d274'],getDataFun:'fun3',echarExample:null, echarData:true,id:'lineEcharts2',title:'产品责任分类'},
]
const data = reactive({
vmmvEcharList:[
{echarData:true,id:'lineEcharts3',title:'事故反馈数'},
{echarData:true,id:'lineEcharts1',title:'责任判定分类'},
{echarData:true,id:'lineEcharts2',title:'产品责任分类'},
],
// 图表配置
EcharOption : {
// 图例标识
legend: {
type:'plain',
icon: 'circle',
itemWidth: 10,
// 图例之间的间隔
itemGap: 10,
top: 0,
right: 16,
textStyle:{
color:'#1d2129'
}
},
// 直角坐标系距离画布边缘距离
grid: {
top: 25,
left: 80,
right: 40,
bottom: 16,
},
// 提示框组件
tooltip: {
// 触发模式:坐标轴触发
trigger: 'axis',
show: true,
renderMode: 'html',
backgroundColor:'#fff',
padding:[12,16],
extraCssText: 'box-shadow: 0 0 3px rgba(2, 9, 25, 0.04);',
textStyle: {
fontSize: 12,
color:'#1d2129',
},
},
color: [],
xAxis: {
type: 'value',
// 不显示 X 轴
axisLine: { show: false },
// 不显示坐标刻度位置
axisTick: {show: false },
// 不显示坐标刻度数值
axisLabel: { show: false },
// 在坐标系中的刻度线
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
width: 1,
},
},
},
yAxis:[
{
type: 'category',
data: [], //lineyDate
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
show: true,
color: '#86909C',
fonSize: 12,
interval: 0,
},
splitLine: { show: false },
},
{
data: [],
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
color: '#333'
}
}
],
series: [],
}
});
onMounted(() => {
window.addEventListener('resize', refreshMyCharAll)
});
onBeforeUnmount(() => {
window.removeEventListener('resize', refreshMyCharAll)
})
// 获取数据
const afferent = () => {
// 遍历数组获取数据
echarList.forEach((item,index)=>{
Api[item.getDataFun](props.searchData).then(result=>{
if (Number(result.code) === 1) {
if(!result.data || Object.keys(result.data).every(item=>result.data[item].length==0)){
item.echarData=false
data.vmmvEcharList[index].echarData=false
}else{
item.echarData=result.data
data.vmmvEcharList[index].echarData=result.data
}
nextTick(()=>{
refreshMyChar(item)
})
}
})
})
}
// 遍历数组整体重置图表
const refreshMyCharAll=()=>{
echarList.forEach(item=>{
refreshMyChar(item)
})
}
// 单一图表重置初始化
const refreshMyChar = (item ) => {
if(!item.echarData ){
if(item.echarExample){
item.echarExample.dispose();
item.echarExample = null;
}
return
}
let linebarDate=item.formatFun(item.echarData)
let chartDom = document.getElementById(item.id)
let option = JSON.parse(JSON.stringify(data.EcharOption));
let barList = changeBarDate(linebarDate);
option.yAxis[0].data = [...barList.yDate];
option.yAxis[1].data = [...barList.yDateList];
option.series = [...barList.lineList];
option.color = item.colorArr;
option = JSON.parse(JSON.stringify(option));
console.log('------echarts图表数据START-------')
console.log(item.title)
console.log(option)
console.log('------echarts图表数据END----11---')
let timeObj = echarts.init(chartDom);
setTimeout(() => {
timeObj.hideLoading()
timeObj.setOption(option);
timeObj.resize()
item.echarExample= timeObj
}, 100);
}
// 数据格式整理函数
const changeBarDate = (linebarDate) => {
let yName = []; //y轴name
let yList = []; //y轴总计
let dateList = {}; //bar数组
let lineName = []; //bar数组名称
for (let i in linebarDate) {
for (let k in linebarDate[i]) {
lineName.push(linebarDate[i][k]);
}
}
// 名称去重
yName = [...new Set(lineName.map(item=>item.name))]
yList = new Array(yName.length).fill(0);
for (let i in linebarDate) {
dateList[i] = new Array(yName.length).fill(0);
for (let k in yName) {
linebarDate[i].map((item)=> {
if (item.name == yName[k]) {
dateList[i][k] = item.value;
yList[k] += item.value
}
return "";
});
}
}
let lineList = [];
// 排序
for (let i = 0; i < yList.length; i++) {
for (let j = i + 1; j < yList.length; j++) {
if (yList[i] > yList[j]) {
var value = yList[i];
var name = yName[i];
yList[i] = yList[j];
yName[i] = yName[j];
yList[j] = value;
yName[j] = name;
for (let k in dateList) {
var itemValue = dateList[k][i];
dateList[k][i] = dateList[k][j];
dateList[k][j] = itemValue;
}
}
}
}
for (let i in dateList) {
lineList.push({
name: i,
type: 'bar',
barMaxWidth: '13px',
stack: 'total',
emphasis: {
disabled: true,
focus: 'series',
},
data: dateList[i],
});
}
return { yDate: yName, yDateList: yList, lineList }
}
defineExpose({
afferent
});
</script>
<style lang="scss" scoped>
.container{
background-color: #fff;
border-radius: 8px;
:deep(.zlgs-empty__description){
margin-top: 0px;
}
}
.echartsbox{
margin-top: 12px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
> div {
width: calc((100% - 24px) / 3 );
height: 332px;
box-sizing: border-box;
}
.echart{
height:295px;
}
}
</style>
感觉像是初始化时 DOM 未 ready
nextTick(() => {
requestAnimationFrame(() => {
chart.setOption(option);
chart.resize();
});
});
把setTimeout换一下看看?
基于提供的代码和现象分析,ECharts 图表偶现乱码问题可能是由以下几个关键原因引起:
响应式数据污染
__ob__ 等隐藏属性数据引用共享
let option = JSON.parse(JSON.stringify(data.EcharOption));
// 后续直接修改了引用对象的属性
option.yAxis[0].data = [...barList.yDate]; // 响应式污染风险异步渲染时序问题setTimeout(100) 可能导致:
const refreshMyChar = (item) => {
// ...数据处理逻辑
// 深度克隆所有数据源
const pureOption = structuredClone(data.EcharOption); // 替代JSON方案
const pureBarList = structuredClone(barList);
// 创建全新非引用对象
const finalOption = {
...pureOption,
yAxis: [
{ ...pureOption.yAxis[0], data: [...pureBarList.yDate] },
{ ...pureOption.yAxis[1], data: [...pureBarList.yDateList] }
],
series: pureBarList.lineList.map(item => ({...item})),
color: [...item.colorArr]
};
// 销毁旧实例前先做判断
if(item.echarExample && !item.echarExample.isDisposed()){
item.echarExample.dispose();
}
nextTick(() => {
const chartDom = document.getElementById(item.id);
if(!chartDom) return; // DOM不存在检查
const chart = echarts.init(chartDom);
chart.setOption(finalOption);
item.echarExample = chart;
chart.resize(); // 移除setTimeout
});
}
在 changeBarDate 返回处添加检查:
// 在 return { yDate, yDateList, lineList } 前添加:
console.assert(
!yDate.__ob__ && !yDateList.__ob__,
'响应式污染警报!',
{ yDate, yDateList }
);
在入口文件添加:
// main.js
import { config } from '@vue/reactivity'
config.unwrapHandlers = {
...config.unwrapHandlers,
// 禁止响应系统劫持特定属性
skip: ['constructor', 'toString', 'hasOwnProperty']
}
执行诊断命令:
npx echarts-diagnose --version 5.5.0
检查是否存在 已知Vue兼容性问题
移除所有 setTimeout,改用:
nextTick(() => {
requestAnimationFrame(() => {
// 渲染逻辑
})
})
最小化复现:
<!-- test.html -->
<div id="debug-chart"></div>
<script>
const data = { constructor: "test", toString: 123 }; // 污染数据
const chart = echarts.init(document.getElementById('debug-chart'));
chart.setOption({ xAxis: {data}, series: [{data: [1,2,3]}] });
</script>监控原型链访问:
const PROTOTYPE_PROPS = Object.getOwnPropertyNames(Object.prototype);
const handler = {
get(target, prop) {
if(PROTOTYPE_PROPS.includes(prop)) console.error('原型访问', prop);
return Reflect.get(...arguments);
}
};
// 包裹option
chart.setOption(new Proxy(option, handler));若问题仍偶现,在图表容器添加CSS保护层:
.echart::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
pointer-events: none;
background: url('data:image/svg+xml;utf8,<svg ...></svg>'); /* 透明防护层 */
}
重要提示:Vue 3.3+ 与 ECharts 5.5 存在特定的响应式冲突,建议优先尝试升级到 ECharts 5.6.0+ 版本验证是否解决根本问题
问题内容: 与Cipher合作时,我观察到以下内容。 加密码: 解密代码: 运行Decrypt代码时,出现IllegalBlockSizeException(输入长度必须为16的倍数)。 但是如果我将解密代码更改为 它工作正常。我知道这是有规律的。所以我以为是因为我没有提到填充。所以我尝试在加密过程中提供模式和填充, 加密码: 解密代码: 但是它失败,并带有IllegalBlockSizeEx
问题内容: 当我尝试运行程序时,出现以下错误 请帮忙 问题答案: 从Javadoc: 如果Java虚拟机找不到声明为native的方法的适当本机语言定义,则抛出该异常。 这是与JNI相关的错误。loadJacobLibrary试图加载名为jacob-1.14.3-x86的本机库,但在java.library.path定义的路径上找不到该库。启动JVM时,应将此路径定义为系统属性。例如 在Windo
在这里抛出RejectedExecutionException是否有其他原因? java.util.concurrent.RejectedExecutionException:任务java.util.concurrent.FutureTask@4194a5f0被java.util.concurrent.ThreadPoolExecutor@41a36e90拒绝[终止,池大小=0,活动线程=0,排队
我想知道=_运算符在JavaScript中的含义。看起来像是在做作业。 示例:
我有一个使用activeMQ消息的项目。它运行良好,但有时会遇到挂起的消息卡在队列中。它说1000入队,0出队,1000分派。它还说1000条待处理的消息。 “待定消息”的可能原因是什么?
本文向大家介绍为什么选择茉莉而不是开玩笑是什么原因?,包括了为什么选择茉莉而不是开玩笑是什么原因?的使用技巧和注意事项,需要的朋友参考一下 没有充分的理由选择茉莉而不是开玩笑。两者都是优秀的库,已经存在了一段时间,并且以一种自以为是的方式来处理非常相似的事情。笑话是建立在茉莉花之上的。 选择茉莉而不是开玩笑的原因之一是它更快。(https://github.com/facebook/jest/is
腾讯第一次二面,面试官没开摄像头,问的东西感觉很偏,有几个没答上来,一周后自动挂。 腾讯第二次二面,面试官上来给了一个题,我秒了之后,感觉面试官对我的兴趣不大,问题像挤牙膏,我回答了之后还沉默了一段时间。 高德地图时隔一个月之后捞我起来二面,全程聊天,没有八股没有题,过两天挂。 美团第一次二面,面试官问我来了能不能写文章,我说不,之后问的问题比较简单,但是算法我一直有bug。 美团第二次二面,面试