10 分钟上手 ECharts:从“能跑”到“生产级”的完整踩坑之旅

10 分钟上手 ECharts:从"能跑"到"生产级"的完整踩坑笔记

如果你也曾 复制了官方 Demo 却不知道怎么拆窗口一拉伸图表就变形切换标签页后内存暴涨 ------这篇博客就是为你写的。

我会用 6 个递进版本 的源码,带你把一张 最简柱状图 逐步进化成 可销毁、可重建、零泄漏 的响应式组件,顺便把 ECharts 的核心 API 一次性讲透。


这里先附上 echart官网

请先耐心阅读文章,文章末附上的有完整源码

00 前言:为什么又写一篇 ECharts 入门?

ECharts 的官方例子足够漂亮,但大多数教程只停在 "hello world" 级别:

javascript 复制代码
echarts.init(dom).setOption(option);

然而真实业务里,我们至少要回答三个问题:

  1. 窗口拉伸怎么办?
  2. 弹窗/标签页切换后图表不见了,再打开为何一片空白?
  3. 反复进出页面,内存为何节节攀升?

今天用 不到 120 行代码 把这三个坑填平,让你 copy-paste 即可投产


01 最小可运行版本(Step-1)------先跑起来再说

文件:step1-hello.html

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>ECharts Step1</title>
  <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
  <style>
    #main{height:60vh;background:pink;}
  </style>
</head>
<body>
  <div id="main"></div>

  <script>
    // 1. 描述你要画什么
    const option = {
      title: { text: 'First ECharts' },
      tooltip: {},
      legend: { data: ['销量'] },
      xAxis: { data: ['衬衫','羊毛衫','裤子','袜子','高跟鞋'] },
      yAxis: {},
      series: [{ name: '销量', type: 'bar', data: [5,20,36,10,10] }]
    };

    // 2.  init → setOption 两行经典 API
    const myChart = echarts.init(document.getElementById('main'));
    myChart.setOption(option);
  </script>
</body>
</html>

initsetOption 是 ECharts 的"开机键"和"遥控器",一句话就能记住:

init 用于创建图表实例,并指定渲染所需的 DOM 节点;

setOption 用于向该实例传入配置项,以生成并更新图表。

必须先执行 init 获得实例,再调用 setOption,否则无法渲染。

此时打开浏览器,粉色区域出现柱状图------任务完成 ,但别急着提交代码,因为拉伸窗口图表不会跟着变


02 让图表"长"在窗口上(Step-2)------响应式 101

ECharts 暴露的唯一武器是:resize()

我们只需在窗口尺寸变化时调用它。

关键细节addEventListenerremoveEventListener 必须指向同一个函数引用,否则解绑失败 → 内存泄漏。

文件:step2-resize.html

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>ECharts Step2</title>
  <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
  <style>
    #main{height:60vh;background:pink;}
  </style>
</head>
<body>
  <div id="main"></div>

  <script>
    const option = {
      title: { text: 'First ECharts' },
      tooltip: {},
      legend: { data: ['销量'] },
      xAxis: { data: ['衬衫','羊毛衫','裤子','袜子','高跟鞋'] },
      yAxis: {},
      series: [{ name: '销量', type: 'bar', data: [5,20,36,10,10] }]
    };
    const myChart = echarts.init(document.getElementById('main'));
    myChart.setOption(option);

    /* 统一句柄:后面销毁时还要用 */
    const handleResize = () => myChart && myChart.resize();
    window.addEventListener('resize', handleResize);
  </script>
</body>
</html>

为什么这个匿名函数要这样写?
const handleResize = () => myChart && myChart.resize();

JavaScript 的 && 运算符具备短路特性 :左侧表达式为真时,才继续执行右侧;左侧为假时,整个表达式立即返回假,右侧代码不会被执行

resize 场景下,左侧的 myChart 若因销毁而变为 null,右侧的 resize() 调用就会被自动跳过,从而避免空指针错误,实现**一行代码完成"存在判断 + 方法调用"**的防御式逻辑。

现在拉伸窗口,柱子实时重排,响应式闭环达成


03 弹窗关闭 ≠ 直接 remove DOM(Step-3)------销毁实例

场景

  • 标签页切换、弹窗关闭、路由跳转 → 容器节点被移除。
  • 用户再次打开弹窗,发现图表区域空白 ,控制台报 Cannot read properties of null

原因
dispose() 没调用,ECharts 实例还在旧 DOM 碎片 里,内存没释放节点已不存在

官方原话

"在容器节点被销毁时,总是 应调用 echartsInstance.dispose 以销毁实例释放资源,避免内存泄漏。"

文件:step3-dispose.html

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>ECharts Step3</title>
  <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
  <style>
    #main{height:60vh;background:pink;}
  </style>
</head>
<body>
  <div id="main"></div>
  <button id="ctrl">销毁</button>

  <script>
    const option = { ... };   // 同上
    const myChart = echarts.init(document.getElementById('main'));
    myChart.setOption(option);
    const handleResize = () => myChart && myChart.resize();
    window.addEventListener('resize', handleResize);

    /* 释放内存的逻辑 */
    const destroyChart = () => {
      if (myChart) {
        myChart.dispose();   // 释放 WebGL/Canvas 资源
        myChart = null;      // 告诉垃圾回收器"我清空了"
        window.removeEventListener('resize', handleResize);
      }
    };

    document.getElementById('ctrl').addEventListener('click', destroyChart);
  </script>
</body>
</html>

最佳实践
谁先删 DOM,谁负责 dispose ;Vue/React 在 beforeUnmountuseEffect cleanup 里统一销毁。


04 一键"销毁/重建"开关(Step-4)------完整切换逻辑

把销毁/创建封装成两个纯函数,再用按钮模拟"标签页切换":

文件:step4-toggle.html

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>ECharts Step4</title>
  <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
  <style>
    #main{height:60vh;background:pink;}
  </style>
</head>
<body>
  <div id="main"></div>
  <button id="ctrl">销毁</button>

  <script>
    const option = { ... };   // 同上
    let myChart = null;
    let exist = true;                       // 当前是否存在
    const btn = document.getElementById('ctrl');

    const createChart = () => {
      myChart = echarts.init(document.getElementById('main'));
      myChart.setOption(option);
      window.addEventListener('resize', handleResize);
    };

    const destroyChart = () => {
      if (myChart) {
        myChart.dispose();
        myChart = null;
        window.removeEventListener('resize', handleResize);
      }
    };

    btn.addEventListener('click', () => {
      exist ? destroyChart() : createChart();
      exist = !exist;
      btn.innerText = exist ? '销毁' : '创建';
    });

    createChart();   // 首次自动创建
  </script>
</body>
</html>
  • 第一次点击 → 销毁(按钮文字变"创建")
  • 第二次点击 → 重建(按钮文字变"销毁")

内存监控 :Chrome DevTools → Memory → Heap snapshot,反复切换,节点数不再上涨


05 算法级优化(Step-Final)------终身只绑一次 resize

大体写完了,但细节和性能上我们还可以进行优化,比如通过引入三元运算符或者封装函数等方式来优化性能

问题

每次重建都 addEventListener → 理论上会重复绑定同一类型事件(虽然浏览器会去重,但仍不优雅)。

思路
resize 监听与图表生命周期脱钩 ,只要全局存在一次即可;内部用"懒调度"判断实例是否存在。

文件:step-final.html

html 复制代码
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>ECharts Step-Final</title>
  <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
  <style>
    #main{height:60vh;background:pink;}
  </style>
</head>
<body>
  <div id="main"></div>
  <button id="ctrl">销毁</button>

  <script>
    const option = { ... };   // 同上
    let myChart = null;
    let exist = true;
    const btn = document.getElementById('ctrl');

    /* 终身只绑一次 resize,无论有多少图表 */
    window.addEventListener('resize', () => myChart && myChart.resize());

    const createChart = () => {
      myChart = echarts.init(document.getElementById('main'));
      myChart.setOption(option);
    };

    const destroyChart = () => {
      myChart && myChart.dispose();
      myChart = null;
    };

    btn.addEventListener('click', () => {
      exist ? destroyChart() : createChart(); // 表格存在就执行销毁,不存在就执行创建
      exist = !exist;
      btn.innerText = exist ? '销毁' : '创建';
    });

    createChart();   // 首次自动创建
  </script>
</body>
</html>

复杂度从 O(绑+解)×NO(1)多图表、路由切换、弹窗堆叠场景下同样适用。


06 直接投产:最终 120 行模板(up主写的完整原生前端三剑客代码)

html 复制代码
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title>echart practice</title>
		<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
	</head>
	<style>
		#main,html,body {
			width: 100%;
		}

		#main {
 			height: 400px;
		}
</style>
	<body>
		<!-- 准备的一个定义好宽高背景色的DOM容器 -->
		<div id="main" style="background-color: pink;"></div>
		<button id="ctrl">销毁</button>
		<!-- 为echart初始化实例 -->
		<script type="text/javascript">
			// type后面那一坨都是老版html需要写的,用于指定脚本语言,现在HTML5可以省略了

			// TODO.1 相关变量配置
			// 表格
			let myChart = null

			// 配置项
			let option = {
				// 标题组件
				title: {
					text: 'First Echart Practice', // 主标题文本
					subtext: '副标题', // 副标题
					// left & right & center 用来控制水平位置
					// top 用于控制垂直位置
				},  
				
				// 提示框组件()鼠标悬停时弹出
				tooltip: {
					trigger: 'item',            // 触发方式:'item'(单点) | 'axis'(坐标轴) | 'none'
					// formatter: '{b}<br/>{a}: {c}' // 自定义浮层内容,模板或回调函数
				},
				 
				// 图例组件(点击可控制系列显隐)
				legend: {
    					data: ['销量'],             // 必须与 series[i].name 保持一致,才能对应
					// orient: 'horizontal',     // 排列方向:'horizontal'|'vertical'
					// left: 'right',            // 位置,同 title
  				},
				 
				// X 轴
				xAxis: {
					data: ['衬衫', '羊毛衫', '裤子', '袜子', '高跟鞋']
				},
				 
				// Y 轴
				yAxis: {},

				// 系列列表(真正决定"画什么图")
				series: [
					{
						name: '销量',             // 与 legend.data 对应,悬停提示也会用 
						type: 'bar', 
						data: [5, 20, 36, 10, 10,20]
					}
				],
			}

			// TODO.2 监听页面大小变化事件
			const handleResize = () => myChart && myChart.resize() // 防御式写法,如果myChart已被销毁就短路返回,不会执行 resize();如果myChart存在,正常调resize()让图表随窗口大小重绘。

			// TODO.3 表格创建初始化函数
			const createChart = () => {
				myChart = echarts.init(document.getElementById('main'))
				console.log('表格对象实例化完成')
				myChart.setOption(option)
				console.log('表格对象展示完成')
				window.addEventListener('resize', handleResize) // 浏览器原生事件,当窗口(window)大小发生变化 时触发
				console.log('表格已建立')
			}

			// TODO.4 销毁实例
			const destroyChart = () => {
				myChart.dispose();   // 释放内存
				myChart = null;      // 垃圾回收,将变量制空
				window.removeEventListener('resize', handleResize);
				console.log('图表已销毁');
			}

			createChart()

			// TODO.5 销毁和创建实例
			let ctrlFactor = true // 为true时表格存在
			const btn = document.getElementById('ctrl')
			console.log('初始化创建成功')
			btn.addEventListener('click', () => {
				btn.innerText = ctrlFactor ? '创建' : '销毁' // 通过控制因子判断按钮文字内容
				ctrlFactor ? destroyChart() : createChart() // 通过控制因子去判断表格操作
				ctrlFactor = !ctrlFactor
			})
		</script>
	</body>
</html>

复制→保存→打开浏览器,你就拥有了一个:

  • 响应式
  • 可销毁/重建
  • 零内存泄漏

的 ECharts 基准模板,后续只需替换 option 即可快速出图!


07 结语:把模板塞进你的脚手架

  • Vue :在 onMounted 调用 createChartonUnmounted 调用 destroyChart
  • React :在 useEffect(() => { createChart(); return destroyChart; }, []); 即可。
  • 多图表 :把 myChart 换成数组或 Map,resize 监听仍只需一次。

至此,内存泄漏、响应式、销毁重建 三大痛点全部解决;

剩下的,就是去 ECharts 官方示例 里复制更炫的 option 了!

Happy charting! 🎉


如果有任何疑问,欢迎在评论区留言讨论!

相关推荐
東雪蓮☆3 小时前
从零开始掌握 Web 与 Nginx:入门详解
运维·服务器·前端·nginx
脑子慢且灵3 小时前
【JavaWeb】一个简单的Web浏览服务程序
java·前端·后端·servlet·tomcat·web·javaee
柯南二号3 小时前
【大前端】 断点续传 + 分片上传(大文件上传优化) 的前端示例
前端
前端小超超3 小时前
如何配置capacitor 打包的安卓app固定竖屏展示?
android·前端·gitee
xiaopengbc3 小时前
在Webpack中,如何在不同环境中使用不同的API地址?
前端·webpack·node.js
前端AK君3 小时前
React中台系统如何嵌入到业务系统中
前端
Slice_cy3 小时前
不定高虚拟列表
前端
前端AK君3 小时前
React组件库如何在vue项目中使用
前端
Moonbit4 小时前
MoonBit 再次走进清华:张宏波受邀参加「思源计划」与「程序设计训练课」
前端·后端·编程语言