当前位置: 首页 > 知识库问答 >
问题:

javascript - echarts 图表出现乱码,而且是偶现,是什么原因,排查思路是什么?

柳昊焱
2025-10-31

bug 现象如图:
image.png

看上去像数据问题,但它的出现是偶现的,相同的条件,刷新页面后就恢复正常了。而且多出来的英文明显不是业务产生的乱码,像是代码里什么对象的方法属性。

根据以往的经验,所谓的偶现,大概率是还没有发现规律,但常见的接口响应时间,访问其他页面残留的全局设置等原因都试过了,还是没发现规律。

而且在本地开发过程中没遇到过,页面是从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>

共有2个答案

龙弘济
2025-10-31

感觉像是初始化时 DOM 未 ready

nextTick(() => {
  requestAnimationFrame(() => {
    chart.setOption(option);
    chart.resize();
  });
});

把setTimeout换一下看看?

夔波
2025-10-31

基于提供的代码和现象分析,ECharts 图表偶现乱码问题可能是由以下几个关键原因引起:

核心原因分析

  1. 响应式数据污染

    • Vue 响应式系统会给对象添加 __ob__ 等隐藏属性
    • ECharts 在深度遍历 option 对象时,可能意外访问到 Vue 注入的响应式元数据
    • 乱码内容如 "constructor"、"toString" 等正是对象原型链上的属性名
  2. 数据引用共享

    let option = JSON.parse(JSON.stringify(data.EcharOption));
    // 后续直接修改了引用对象的属性
    option.yAxis[0].data = [...barList.yDate]; // 响应式污染风险
  3. 异步渲染时序问题
    setTimeout(100) 可能导致:

    • 组件已销毁但图表仍在初始化
    • 多图表实例竞争全局资源

针对性的排查解决方案

1. 深度数据隔离(关键修复)

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
  });
}

2. 响应式污染检测

changeBarDate 返回处添加检查:

// 在 return { yDate, yDateList, lineList } 前添加:
console.assert(
  !yDate.__ob__ && !yDateList.__ob__,
  '响应式污染警报!',
  { yDate, yDateList }
);

3. 全局防污染配置

在入口文件添加:

// main.js
import { config } from '@vue/reactivity'
config.unwrapHandlers = {
  ...config.unwrapHandlers,
  // 禁止响应系统劫持特定属性
  skip: ['constructor', 'toString', 'hasOwnProperty'] 
}

4. ECharts 版本验证

执行诊断命令:

npx echarts-diagnose --version 5.5.0

检查是否存在 已知Vue兼容性问题

5. 时序问题修复

移除所有 setTimeout,改用:

nextTick(() => {
  requestAnimationFrame(() => {
    // 渲染逻辑
  })
})

验证建议

  1. 最小化复现

    <!-- 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>
  2. 监控原型链访问

    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。 美团第二次二面,面试