[ECharts] Instance ec_1234567890 has been disposed

📋 目录


🔍 问题背景

在 Vue 3 项目中使用 ECharts 时,经常会遇到以下控制台警告:

csharp 复制代码
[ECharts] Instance ec_1234567890 has been disposed

这个警告虽然不会影响功能,但表明存在潜在的内存泄漏问题。

问题原因

  1. 图表实例已销毁,但事件监听器仍在运行

    • 调用 chart.dispose() 销毁图表后
    • window.resize 事件监听器仍然存在
    • 监听器尝试调用已销毁实例的 chart.resize() 方法
    • 导致 ECharts 输出警告信息
  2. 重复添加事件监听器

    • 每次重新渲染图表时都添加新的 resize 监听器
    • 旧的监听器没有被清理
    • 导致内存泄漏和事件堆积

⚠️ 常见问题

问题 1:直接销毁图表实例

javascript 复制代码
// ❌ 错误做法
if (chartInstance) {
  chartInstance.dispose(); // 直接销毁,但 resize 监听器还在
}

const chart = echarts.init(container);
window.addEventListener("resize", () => {
  chart.resize(); // 监听器引用了图表实例
});

问题

  • 销毁图表后,resize 监听器仍然存在
  • 监听器尝试调用已销毁实例的方法
  • 产生 "has been disposed" 警告

问题 2:重复添加监听器

javascript 复制代码
// ❌ 错误做法
function renderChart() {
  const chart = echarts.init(container);

  // 每次调用都添加新监听器
  window.addEventListener("resize", () => {
    chart.resize();
  });
}

// 多次调用导致监听器堆积
renderChart(); // 添加第 1 个监听器
renderChart(); // 添加第 2 个监听器
renderChart(); // 添加第 3 个监听器

问题

  • 每次渲染都添加新的监听器
  • 旧的监听器没有被清理
  • 导致内存泄漏

✅ 解决方案

核心思路

在销毁图表实例前,先移除所有相关的事件监听器

实现步骤

1. 存储图表实例和监听器

javascript 复制代码
import { ref } from "vue";

// 存储图表实例
const chartInstances = ref({});

// 存储每个图表的 resize 处理函数
const resizeHandlers = ref({});

2. 渲染图表时正确管理监听器

javascript 复制代码
const renderChart = (data, containerId) => {
  nextTick(() => {
    const container = document.getElementById(containerId);
    if (!container) return;

    // ✅ 步骤 1:清理旧实例
    if (chartInstances.value[containerId]) {
      // 先移除旧的 resize 监听器
      if (resizeHandlers.value[containerId]) {
        window.removeEventListener("resize", resizeHandlers.value[containerId]);
      }
      // 再销毁图表实例
      chartInstances.value[containerId].dispose();
    }

    // ✅ 步骤 2:创建新实例
    const chartInstance = echarts.init(container);
    chartInstances.value[containerId] = chartInstance;

    // ✅ 步骤 3:配置并渲染图表
    const option = {
      // ... 图表配置
    };
    chartInstance.setOption(option);

    // ✅ 步骤 4:添加 resize 监听器并存储
    const resizeHandler = () => {
      chartInstance.resize();
    };
    resizeHandlers.value[containerId] = resizeHandler;
    window.addEventListener("resize", resizeHandler);
  });
};

3. 组件卸载时完整清理

javascript 复制代码
import { onBeforeUnmount } from "vue";

onBeforeUnmount(() => {
  // ✅ 步骤 1:移除所有 resize 监听器
  Object.entries(resizeHandlers.value).forEach(([containerId, handler]) => {
    if (handler) {
      window.removeEventListener("resize", handler);
    }
  });

  // ✅ 步骤 2:销毁所有图表实例
  Object.values(chartInstances.value).forEach(chart => {
    if (chart) {
      chart.dispose();
    }
  });

  // ✅ 步骤 3:清空引用
  chartInstances.value = {};
  resizeHandlers.value = {};
});

💻 完整代码示例

Vue 3 组件示例

vue 复制代码
<script setup>
import { ref, watch, nextTick, onBeforeUnmount } from "vue";
import * as echarts from "echarts";

const props = defineProps({
  data: { type: Array, default: () => [] },
  loading: { type: Boolean, default: false },
});

// 存储图表实例
const chartInstances = ref({});

// 存储每个图表的 resize 处理函数
const resizeHandlers = ref({});

// 渲染图表
const renderChart = (chartData, containerId) => {
  nextTick(() => {
    const container = document.getElementById(containerId);
    if (!container) return;

    // 如果已存在图表实例,先清除监听器再销毁
    if (chartInstances.value[containerId]) {
      // 移除旧的 resize 监听器
      if (resizeHandlers.value[containerId]) {
        window.removeEventListener("resize", resizeHandlers.value[containerId]);
      }
      // 销毁图表实例
      chartInstances.value[containerId].dispose();
    }

    // 初始化 ECharts 实例
    const chartInstance = echarts.init(container);
    chartInstances.value[containerId] = chartInstance;

    // 配置图表选项
    const option = {
      title: { text: "示例图表" },
      tooltip: { trigger: "axis" },
      xAxis: { type: "category", data: chartData.map(item => item.name) },
      yAxis: { type: "value" },
      series: [
        {
          type: "bar",
          data: chartData.map(item => item.value),
        },
      ],
    };

    // 渲染图表
    chartInstance.setOption(option);

    // 监听窗口大小变化,自动调整图表大小
    const resizeHandler = () => {
      chartInstance.resize();
    };
    // 存储 resize 处理函数,以便后续清理
    resizeHandlers.value[containerId] = resizeHandler;
    window.addEventListener("resize", resizeHandler);
  });
};

// 渲染所有图表
const renderAllCharts = () => {
  props.data.forEach((chartData, index) => {
    renderChart(chartData, `chart-${index}`);
  });
};

// 监听数据变化
watch(
  () => [props.data, props.loading],
  ([newData, newLoading]) => {
    if (!newLoading && newData.length > 0) {
      renderAllCharts();
    }
  },
  { deep: true, immediate: true },
);

// 组件卸载时销毁所有图表实例
onBeforeUnmount(() => {
  // 先移除所有 resize 监听器
  Object.entries(resizeHandlers.value).forEach(([containerId, handler]) => {
    if (handler) {
      window.removeEventListener("resize", handler);
    }
  });

  // 再销毁所有图表实例
  Object.values(chartInstances.value).forEach(chart => {
    if (chart) {
      chart.dispose();
    }
  });

  // 清空引用
  chartInstances.value = {};
  resizeHandlers.value = {};
});
</script>

<template>
  <div class="chart-container">
    <div v-for="(chartData, index) in data" :key="index" :id="`chart-${index}`" class="chart"></div>
  </div>
</template>

<style scoped>
.chart-container {
  padding: 20px;
}

.chart {
  width: 100%;
  height: 400px;
  margin-bottom: 20px;
}
</style>

📊 对比分析

错误做法 vs 正确做法

方面 ❌ 错误做法 ✅ 正确做法
监听器管理 直接添加,不存储引用 存储监听器函数引用
销毁顺序 直接销毁图表实例 先移除监听器,再销毁实例
重复渲染 监听器堆积 清理旧监听器后再添加新的
组件卸载 只销毁图表实例 先清理监听器,再销毁实例
内存泄漏 ⚠️ 存在 ✅ 无
控制台警告 ⚠️ 有警告 ✅ 无警告

🎯 最佳实践总结

1. 使用对象存储多个图表实例

javascript 复制代码
// ✅ 推荐:使用对象存储,支持多个图表
const chartInstances = ref({});
const resizeHandlers = ref({});

// ❌ 不推荐:单个变量,不支持多图表
const chartInstance = ref(null);

2. 销毁顺序很重要

javascript 复制代码
// ✅ 正确顺序
// 1. 移除事件监听器
window.removeEventListener("resize", resizeHandler);
// 2. 销毁图表实例
chart.dispose();

// ❌ 错误顺序
// 1. 销毁图表实例
chart.dispose();
// 2. 移除事件监听器(此时监听器可能已经触发)
window.removeEventListener("resize", resizeHandler);

3. 存储监听器函数引用

javascript 复制代码
// ✅ 正确:存储函数引用
const resizeHandler = () => {
  chart.resize();
};
resizeHandlers.value[containerId] = resizeHandler;
window.addEventListener("resize", resizeHandler);

// 后续可以精确移除
window.removeEventListener("resize", resizeHandlers.value[containerId]);

// ❌ 错误:匿名函数无法移除
window.addEventListener("resize", () => {
  chart.resize();
});
// 无法移除这个监听器!

4. 组件卸载时完整清理

javascript 复制代码
onBeforeUnmount(() => {
  // ✅ 完整的清理流程
  // 1. 移除所有监听器
  Object.entries(resizeHandlers.value).forEach(([id, handler]) => {
    if (handler) {
      window.removeEventListener("resize", handler);
    }
  });

  // 2. 销毁所有图表
  Object.values(chartInstances.value).forEach(chart => {
    if (chart) {
      chart.dispose();
    }
  });

  // 3. 清空引用
  chartInstances.value = {};
  resizeHandlers.value = {};
});

5. 使用 nextTick 确保 DOM 已渲染

javascript 复制代码
// ✅ 推荐:使用 nextTick
const renderChart = (data, containerId) => {
  nextTick(() => {
    const container = document.getElementById(containerId);
    if (!container) return;
    // ... 渲染图表
  });
};

// ❌ 不推荐:直接渲染可能找不到 DOM
const renderChart = (data, containerId) => {
  const container = document.getElementById(containerId);
  // container 可能为 null
};

🔧 其他解决方案

方案 1:禁用 ECharts 警告(不推荐)

javascript 复制代码
// ⚠️ 治标不治本,不推荐
echarts.warn = function () {};

缺点

  • 只是隐藏警告,没有解决根本问题
  • 内存泄漏依然存在
  • 失去了 ECharts 的其他有用警告

方案 2:使用 try-catch 静默处理(不推荐)

javascript 复制代码
// ⚠️ 不推荐
try {
  chart.dispose();
} catch (e) {
  // 忽略错误
}

缺点

  • 没有解决监听器泄漏问题
  • 可能隐藏其他真正的错误

方案 3:正确管理监听器(✅ 推荐)

javascript 复制代码
// ✅ 推荐:本文介绍的方案
// 1. 存储监听器引用
// 2. 销毁前先移除监听器
// 3. 组件卸载时完整清理

📚 参考资料


💡 总结

  1. 核心原则:在销毁图表实例前,先移除所有相关的事件监听器
  2. 存储引用:使用对象存储图表实例和监听器函数引用
  3. 正确顺序:先移除监听器 → 再销毁图表 → 最后清空引用
  4. 完整清理:组件卸载时确保所有资源都被正确释放
  5. 避免泄漏:每次重新渲染前清理旧的监听器

遵循这些最佳实践,可以完全避免 ECharts 的 "has been disposed" 警告,并确保没有内存泄漏问题。

相关推荐
xuedaobian17 小时前
Markdown 宽表格突破容器边界滚动方案
前端·css
德育处主任17 小时前
『NAS』中午煮什么?Cook
前端·docker
清风乐鸣17 小时前
Zustand 、Jotai和Valtio源码探析
前端
LawrenceLan17 小时前
Flutter 零基础入门(八):Dart 类(Class)与对象(Object)
前端·flutter
小oo呆17 小时前
【学习心得】Python的Pydantic(简介)
前端·javascript·python
funnycoffee12317 小时前
F5 Big IP如何设置web和SSH登录的白名单
前端·tcp/ip·ssh
JarvanMo17 小时前
国产 App,求你放过我的 iPhone 电量吧!
前端
先飞的笨鸟17 小时前
2026 年 Expo + React Native 项目接入微信分享完整指南
前端·ios·app
angelQ17 小时前
Vercel部署:前后端分离项目的整体部署流程及问题排查
前端·javascript