10 分钟上手 ECharts:从“能跑”到“生产级”的完整踩坑之旅
10 分钟上手 ECharts:从“能跑”到“生产级”的完整踩坑笔记
如果你也曾 复制了官方 Demo 却不知道怎么拆、窗口一拉伸图表就变形、切换标签页后内存暴涨——这篇博客就是为你写的。
我会用 6 个递进版本 的源码,带你把一张 最简柱状图 逐步进化成 可销毁、可重建、零泄漏 的响应式组件,顺便把 ECharts 的核心 API 一次性讲透。
这里先附上 echart官网
请先耐心阅读文章,文章末附上的有完整源码
00 前言:为什么又写一篇 ECharts 入门?
ECharts 的官方例子足够漂亮,但大多数教程只停在 “hello world” 级别:
echarts.init(dom).setOption(option);
然而真实业务里,我们至少要回答三个问题:
- 窗口拉伸怎么办?
- 弹窗/标签页切换后图表不见了,再打开为何一片空白?
- 反复进出页面,内存为何节节攀升?
今天用 不到 120 行代码 把这三个坑填平,让你 copy-paste 即可投产。
01 最小可运行版本(Step-1)——先跑起来再说
文件:step1-hello.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 两行经典 APIconst myChart = echarts.init(document.getElementById('main'));myChart.setOption(option);</script>
</body>
</html>
init
和 setOption
是 ECharts 的“开机键”和“遥控器”,一句话就能记住:
init 用于创建图表实例,并指定渲染所需的 DOM 节点;
setOption 用于向该实例传入配置项,以生成并更新图表。
必须先执行 init 获得实例,再调用 setOption,否则无法渲染。
此时打开浏览器,粉色区域出现柱状图——任务完成,但别急着提交代码,因为拉伸窗口图表不会跟着变。
02 让图表“长”在窗口上(Step-2)——响应式 101
ECharts 暴露的唯一武器是:resize()
我们只需在窗口尺寸变化时调用它。
关键细节:addEventListener
与 removeEventListener
必须指向同一个函数引用,否则解绑失败 → 内存泄漏。
文件:step2-resize.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
<!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 在 beforeUnmount
或 useEffect cleanup
里统一销毁。
04 一键“销毁/重建”开关(Step-4)——完整切换逻辑
把销毁/创建封装成两个纯函数,再用按钮模拟“标签页切换”:
文件:step4-toggle.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
<!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(绑+解)×N
→ O(1)
,多图表、路由切换、弹窗堆叠场景下同样适用。
06 直接投产:最终 120 行模板(up主写的完整原生前端三剑客代码)
<!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
调用createChart
,onUnmounted
调用destroyChart
。 - React:在
useEffect(() => { createChart(); return destroyChart; }, []);
即可。 - 多图表:把
myChart
换成数组或 Map,resize 监听仍只需一次。
至此,内存泄漏、响应式、销毁重建 三大痛点全部解决;
剩下的,就是去 ECharts 官方示例 里复制更炫的 option
了!
Happy charting! 🎉
如果有任何疑问,欢迎在评论区留言讨论!