在Vue3+ECharts中图表tooltip不显示问题分析和解决方法

 更新时间:2026年02月12日 09:51:32   作者:爱看星星的猪_  
文章主要描述了在Vue3中使用ECharts时遇到的tooltip不显示问题,通过分析问题根源和修改方式,最终使用markRaw()工具函数解决了问题,文章详细解释了Vue和ECharts的工作原理以及markRaw的作用,最后提供了完整的修复代码示例,需要的朋友可以参考下

一、问题复现

在开发一个展示“我行信贷五级分类变化趋势”的柱状图时,出现以下情况:

  • 左侧示例图(预期效果):鼠标悬停在某个月份上时,会弹出包含“正常类”“关注类”“不良类”金额的详情浮窗。
  • 右侧实际图(当前问题):虽然图表能正常渲染,但鼠标悬停没有任何反应tooltip 完全不出现。

我已经设置了:

tooltip: {
  trigger: 'axis',
  confine: true,
  axisPointer: { type: 'shadow' }
}

并且数据结构也正确,为什么 tooltip 还是不显示?

二、问题所在

以下是导致问题的核心代码片段:

const myChart = ref(null);
 
const initChart = () => {
  if (!classify.value) return;
  myChart.value = echarts.init(classify.value); // ❌ 问题就在这里!
  const seriesData = [];
  // ... 后续处理数据并 setOption
};

关键错误点分析

  1. myChart 是通过 ref() 创建的响应式引用。
  2. 当执行 myChart.value = echarts.init(...) 时,ECharts 的实例被赋值给了一个 Vue 响应式对象
  3. Vue 会对这个实例进行 Proxy 包装,使其变成响应式代理对象。
  4. ECharts 内部依赖于对象的原始引用(如 === 判断、事件绑定等),一旦被 Proxy 包裹,就会导致:
  • tooltip 触发逻辑失效
  • formatter 函数不执行
  • axisPointer 无法正确定位

所以,尽管写了正确的 tooltip.trigger: 'axis',但由于 ECharts 实例被 Vue “污染”,它根本不知道该什么时候触发提示框!

三、修改方式:使用markRaw解决

修改后的代码(关键改动)

const myChart = ref(null);
 
const initChart = () => {
  if (!classify.value) return;
  myChart.value = markRaw(echarts.init(classify.value)); // ✅ 使用 markRaw 包裹
  const seriesData = [];
  // ... 后续处理数据并 setOption
};

改动说明

原始代码修改后
myChart.value = echarts.init(...)myChart.value = markRaw(echarts.init(...))

markRaw() 是 Vue 提供的一个工具函数,作用是:标记某个值永远不要被转为响应式

四、原理详解:为什么markRaw能解决问题?

1. Vue 的响应式机制

Vue 3 使用 Proxy 实现响应式系统。当你把一个对象赋值给 refreactive 时,Vue 会创建一个代理对象来拦截读写操作。

const obj = { a: 1 }
const reactiveObj = reactive(obj) // ← 现在是 Proxy 对象

2. ECharts 的内部机制

ECharts 在初始化时会做以下事情:

  • 绑定事件监听器(如 mouseover
  • 存储 DOM 元素引用
  • 使用 === 比较对象是否相等(用于判断是否需要重新渲染)
  • tooltip 中通过 instance 引用获取配置

如果实例被 Proxy 包裹,这些比较和绑定就会失败。

3.markRaw的作用

const rawInstance = markRaw(echarts.init(dom))

这行代码告诉 Vue:“这个对象不需要被代理,请原封不动地传下去。”

这样,ECharts 就拿到了一个原始的、未被包装的实例,可以正常运行其内部逻辑。

五、修改后的影响 & 如何应对

正面影响

优势说明
tooltip 正常显示鼠标悬停时弹出详细信息
formatter 函数生效可自定义提示内容
axisPointer 正确工作阴影指示器正常显示
✅ 图表性能提升避免不必要的响应式追踪

潜在影响与解决方案

1.不能直接在模板中绑定myChart.value

因为 myChart.valuemarkRaw 的结果,不是响应式对象,所以:

<!-- ❌ 错误:不能直接在模板中使用 -->
<template>
  <div>{{ myChart.value }}</div> <!-- 会报错或显示 undefined -->
</template>

✅ 解决方案:只在 JS 中使用,不暴露到模板。

2.无法自动响应容器大小变化

如果你希望图表随容器缩放而自动重绘,需手动监听。

✅ 解决方案:使用 ResizeObserver 手动调用 resize()

const resizeObserver = ref(null)
 
onMounted(() => {
  resizeObserver.value = new ResizeObserver(() => {
    myChart.value?.resize()
  })
  resizeObserver.value.observe(classify.value)
})
 
onUnmounted(() => {
  resizeObserver.value?.disconnect()
})

3.生命周期管理仍需注意

即使用了 markRaw,也要记得销毁图表:

onUnmounted(() => {
  myChart.value?.dispose()
})

六、完整示例代码(修复版)

<template>
  <el-card class="customer-risk-card">
    <span class="title">我行信贷五级分类变化趋势(金额:万元)</span>
    <div ref="classify" class="chart"></div>
  </el-card>
</template>
 
<script setup>
import { ref, onMounted, onUnmounted, markRaw } from 'vue'
import * as echarts from 'echarts'
import RwmscenterApi from '@api/rwmscenter/rwmscenterApi.ts'
 
const props = defineProps({
  params: Object,
})
 
const classify = ref(null)
const myChart = ref(null)
const chartData = ref({
  xAxisData: [],
  originData: {},
})
const resizeObserver = ref(null)
 
// 获取图表数据
const getCstRiskFiveGrade = async () => {
  try {
    const res = await RwmscenterApi.getCstRiskFiveGrade({
      cstGycd: props.params.cstGycd,
      cstNo: props.params.cstNo,
    })
    chartData.value.originData = res.body.data
    initChart()
  } catch (error) {
    console.error('获取数据失败:', error)
  }
}
 
// 初始化图表
const initChart = () => {
  if (!classify.value) return
  myChart.value = markRaw(echarts.init(classify.value)) // ✅ 关键:使用 markRaw
  const seriesData = []
 
  for (const key in chartData.value.originData) {
    seriesData.push({
      name: key,
      type: 'bar',
      stack: 'total',
      label: { show: false },
      emphasis: { focus: 'series' },
      data: chartData.value.originData[key].map(item => item.value / 10000),
      barWidth: 16,
    })
  }
 
  const option = {
    colors: [
      { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: '#66DAA8' }, { offset: 1, color: '#fff' }] },
      { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: '#A30559' }, { offset: 1, color: '#fff' }] },
      { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: ?('FFBE5E'), { offset: 1, color: '#fff' }] },
      { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: '#FA95E' }, { offset: 1, color: '#fff' }] },
      { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: '#FFBE5E' }, { offset: 1, color: '#fff' }] },
    ],
    tooltip: {
      trigger: 'axis',
      confine: true,
      axisPointer: { type: 'shadow' },
      formatter: function(params) {
        const date = params[0].axisValue
        let html = `<div style="padding: 8px; background: white; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); font-size: 12px;">`
        html += `<strong>${date}</strong>
`
        params.forEach(param => {
          html += `<span style="color: ${param.color};">●</span> ${param.seriesName}: <strong>${param.value} 万元</strong>
`
        })
        html += '</div>'
        return html
      }
    },
    legend: { right: 'center' },
    grid: { left: '3%', right: '4%', bottom: '8%', containLabel: true },
    yAxis: { type: 'value', name: '', nameTextStyle: { color: '#999999', fontSize: 12 }, axisLabel: { color: '#666666', fontSize: 12 }, splitLine: { show: true, lineStyle: { color: '#EEEEEE' } } },
    xAxis: { type: 'category', axisLine: { show: true, lineStyle: { color: '#F3F4F9' } }, axisTick: { show: false }, axisLabel: { color: '#666666', fontSize: 12 }, data: chartData.value.xAxisData },
    series: seriesData
  }
 
  myChart.value.setOption(option)
}
 
// 监听窗口大小变化
const handlerResize = () => {
  myChart.value?.resize()
}
 
onMounted(() => {
  getCstRiskFiveGrade()
  if (typeof ResizeObserver !== 'undefined') {
    resizeObserver.value = new ResizeObserver(entries => handlerResize())
    resizeObserver.value.observe(classify.value)
  }
})
 
onBeforeUnmount(() => {
  if (myChart.value) myChart.value.dispose()
  if (resizeObserver.value) resizeObserver.value.disconnect()
})
</script>
 
<style lang="less" scoped>
.customer-risk-card {
  .title {
    font-size: 14px;
    font-weight: bold;
    margin-bottom: 10px;
  }
  .chart {
    width: 100%;
    height: 300px;
  }
}
</style>

总结:解决问题的完整路径

步骤操作目的
1️⃣发现 tooltip 不显示定位问题
2️⃣查看代码发现 myChart.value = echarts.init(...)找到根源
3️⃣使用 markRaw 包裹实例防止 Vue 干扰
4️⃣添加 formatter 自定义提示实现期望效果
5️⃣补充 resizedispose完善生命周期管理

结论:当在 Vue 3 中使用 ECharts 时,若遇到 tooltip 无法触发、formatter 不执行等问题,绝大多数情况下是因为 ECharts 实例被 Vue 的响应式系统干扰了
解决方案就是:使用 markRaw(echarts.init(...)),让实例保持“原生状态”。

以上就是在Vue3+ECharts中图表tooltip不显示问题分析和解决方法的详细内容,更多关于Vue3 ECharts图表tooltip不显示的资料请关注脚本之家其它相关文章!

相关文章

  • VueUse使用及造轮子选择对比示例详解

    VueUse使用及造轮子选择对比示例详解

    这篇文章主要为大家介绍了VueUse使用及造轮子选择对比示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • vue常用知识点整理

    vue常用知识点整理

    Vue是一套用于构建用户界面的渐进式JavaScript框架。这篇文章整理了vue中的常用知识点,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-06-06
  • Vue配置marked链接添加target=

    Vue配置marked链接添加target="_blank"的方法

    这篇文章主要介绍了Vue配置marked链接添加target="_blank"的方法,文中给大家提到了vue实现类似target="_blank"打开新窗口的代码,感兴趣的朋友参考下吧
    2019-07-07
  • Vue面试必备之防抖和节流的使用

    Vue面试必备之防抖和节流的使用

    在这篇文章中,大家会了解到如何在 Vue 组件中使用防抖和节流控制 观察者(watchers)以及事件处理程序,文中的示例代码简洁易懂,希望对大家有所帮助
    2023-03-03
  • vue3使用ref的性能警告问题

    vue3使用ref的性能警告问题

    这篇文章主要介绍了vue3使用ref的性能警告问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-04-04
  • vue3学习笔记简单封装axios示例实现

    vue3学习笔记简单封装axios示例实现

    这篇文章主要为大家介绍了vue3学习笔记简单封装axios示例实现,
    2022-06-06
  • Vue watch原理源码层深入讲解

    Vue watch原理源码层深入讲解

    watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用。在面试时,也是必问知识点,一般会用作和 computed 进行比较。那么本文就来带大家从源码理解 watch 的工作流程,以及依赖收集和深度监听的实现
    2022-10-10
  • 装饰者模式在日常开发中的缩影和vue中的使用详解

    装饰者模式在日常开发中的缩影和vue中的使用详解

    这篇文章主要为大家介绍了装饰者模式在日常开发中的缩影和vue中的使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • Vue实现注册页面的用户交互详解

    Vue实现注册页面的用户交互详解

    这篇文章主要为大家详细介绍了Vue实现注册页面的用户交互的相关知识,文中的示例代码讲解详细,对我们深入掌握vue有一定的帮助,需要的小伙伴可以参考下
    2023-12-12
  • JavaScript的Vue.js库入门学习教程

    JavaScript的Vue.js库入门学习教程

    Vue的很多思想借鉴于Angular,但却比较轻量和自由,这里我们整理了JavaScript的Vue.js库入门学习教程,包括其架构思想与核心的数据绑定方式等,需要的朋友可以参考下
    2016-05-05

最新评论