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