ECharts 直接修改option引用陷阱完全指南
文档类型: 反模式警示
难度等级: ⭐⭐
源码版本: ECharts 5.x
本文行数: 约450行
📋 目录
🎯 问题描述
核心问题
ECharts的setOption方法会内部缓存传入的option对象。如果后续直接修改这个对象的引用,会导致:
- ❌ 图表状态不一致
- ❌ 更新不生效
- ❌ 难以排查的bug
typescript
// ❌ 错误示范
const option = {
series: [{ data: [1, 2, 3] }]
};
chart.setOption(option);
// 直接修改option引用
option.series[0].data = [4, 5, 6]; // ⚠️ 危险操作!
// 图表不会自动更新!1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
❌ 错误示例
错误1: 外部修改option
typescript
// 初始化
const option = {
xAxis: { data: ['A', 'B', 'C'] },
series: [{ data: [10, 20, 30] }]
};
chart.setOption(option);
// 一段时间后,尝试更新数据
option.series[0].data = [40, 50, 60]; // ❌ 直接修改
// 图表没有变化!因为ECharts内部仍使用旧引用
console.log(chart.getOption().series[0].data); // [10, 20, 30]1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
错误2: 共享引用导致副作用
typescript
// 多个图表共享同一个option对象
const sharedOption = {
series: [{ type: 'bar', data: [1, 2, 3] }]
};
const chart1 = echarts.init(container1);
const chart2 = echarts.init(container2);
chart1.setOption(sharedOption);
chart2.setOption(sharedOption);
// 修改会影响两个图表的内部状态
sharedOption.series[0].data = [4, 5, 6]; // ❌ 危险!
// chart1和chart2的行为变得不可预测1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
错误3: Vue/React响应式失效
typescript
// Vue示例
const state = reactive({
option: {
series: [{ data: [1, 2, 3] }]
}
});
chart.setOption(state.option);
// 修改响应式数据
state.option.series[0].data = [4, 5, 6]; // ❌ Vue能检测到变化
// 但ECharts不知道数据已变化,不会重新渲染!1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
✅ 正确做法
方案1: 始终调用setOption更新
typescript
// ✅ 正确: 通过setOption更新
let currentData = [1, 2, 3];
chart.setOption({
series: [{ data: currentData }]
});
// 更新时
currentData = [4, 5, 6];
chart.setOption({
series: [{ data: currentData }] // ✅ 重新调用setOption
});1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
方案2: 使用notMerge强制合并
typescript
const newData = [4, 5, 6];
chart.setOption({
series: [{ data: newData }]
}, {
notMerge: true // ✅ 不合并,完全替换
});1
2
3
4
5
6
7
2
3
4
5
6
7
方案3: 深拷贝option
typescript
import _ from 'lodash';
const originalOption = {
series: [{ data: [1, 2, 3] }]
};
// 创建深拷贝
const clonedOption = _.cloneDeep(originalOption);
chart.setOption(clonedOption);
// 修改原始对象不会影响图表
originalOption.series[0].data = [4, 5, 6]; // ✅ 安全1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
方案4: React/Vue中的最佳实践
React
typescript
import { useEffect, useRef } from 'react';
import * as echarts from 'echarts';
function ChartComponent({ data }: { data: number[] }) {
const chartRef = useRef<echarts.ECharts | null>(null);
useEffect(() => {
if (!chartRef.current) return;
// ✅ 每次data变化都创建新对象
const option = {
series: [{
type: 'bar',
data: [...data] // 创建新数组
}]
};
chartRef.current.setOption(option, true);
}, [data]);
return <div ref={(el) => {
if (el && !chartRef.current) {
chartRef.current = echarts.init(el);
}
}} />;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Vue 3
vue
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
import * as echarts from 'echarts';
const props = defineProps<{
data: number[]
}>();
const chartInstance = ref<echarts.ECharts | null>(null);
onMounted(() => {
chartInstance.value = echarts.init(document.getElementById('chart')!);
});
// ✅ 监听数据变化并更新
watch(() => props.data, (newData) => {
if (chartInstance.value) {
chartInstance.value.setOption({
series: [{
data: [...newData] // 创建新数组
}]
});
}
}, { deep: true });
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
🔍 原理分析
ECharts内部缓存机制
typescript
// ECharts内部伪代码
class ECharts {
private _model: Model;
setOption(option: any, notMerge?: boolean) {
if (notMerge) {
// 完全替换
this._model = new Model(option);
} else {
// 合并到现有model
this._model.mergeOption(option);
}
// 保存option引用用于后续对比
this._cachedOption = option; // ⚠️ 这里保存了引用!
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
问题根源:
- ECharts为了性能,会缓存option引用
- 直接修改外部对象,ECharts无法感知变化
- 需要显式调用
setOption触发更新
响应式框架的局限
typescript
// Vue/React能检测到数据变化
state.data = [4, 5, 6]; // ✅ Vue检测到
// 但ECharts是独立的库,不会自动订阅这些变化
chart.setOption(/* ... */); // ❌ 必须手动调用1
2
3
4
5
2
3
4
5
解决方案:
- 在Vue的
watch或React的useEffect中调用setOption - 确保传递新的对象引用
💻 实战案例
案例1: 动态数据更新
typescript
class DataUpdater {
private chart: echarts.ECharts;
private currentData: number[] = [];
constructor(container: HTMLElement) {
this.chart = echarts.init(container);
this.init();
}
private init() {
this.chart.setOption({
xAxis: { type: 'category' },
yAxis: { type: 'value' },
series: [{
type: 'line',
data: this.currentData
}]
});
}
/**
* ✅ 正确: 通过方法更新数据
*/
updateData(newData: number[]) {
this.currentData = newData;
// 创建新option对象
this.chart.setOption({
series: [{
data: [...this.currentData] // 展开运算符创建新数组
}]
});
}
/**
* ❌ 错误: 直接修改
*/
updateDataWrong(newData: number[]) {
this.currentData = newData;
// 获取当前option
const option = this.chart.getOption();
// 直接修改 - 危险!
option.series[0].data = newData; // ❌
// 即使再次setOption也可能有问题
this.chart.setOption(option); // ⚠️ 引用相同
}
dispose() {
this.chart.dispose();
}
}
// 使用
const updater = new DataUpdater(document.getElementById('chart')!);
updater.updateData([1, 2, 3, 4, 5]); // ✅ 正确方式1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
案例2: 定时器更新
typescript
class RealtimeChart {
private chart: echarts.ECharts;
private timer: number | null = null;
constructor(container: HTMLElement) {
this.chart = echarts.init(container);
this.start();
}
private start() {
let data = Array.from({ length: 10 }, () => Math.random() * 100);
// 初始渲染
this.chart.setOption({
series: [{ data }]
});
// 定时更新
this.timer = window.setInterval(() => {
// 生成新数据
data.shift();
data.push(Math.random() * 100);
// ✅ 正确: 创建新数组
this.chart.setOption({
series: [{ data: [...data] }]
});
// ❌ 错误: 不要这样做
// const option = this.chart.getOption();
// option.series[0].data = data;
// this.chart.setOption(option);
}, 1000);
}
stop() {
if (this.timer !== null) {
clearInterval(this.timer);
this.timer = null;
}
}
dispose() {
this.stop();
this.chart.dispose();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
🎯 最佳实践总结
✅ DO - 推荐做法
始终通过setOption更新
typescriptchart.setOption({ series: [{ data: newData }] });1创建新对象/数组
typescriptdata: [...newData] // 新数组 option: { ...oldOption } // 新对象1
2使用notMerge完全替换
typescriptchart.setOption(newOption, { notMerge: true });1
❌ DON'T - 避免做法
不要直接修改option引用
typescript// ❌ 绝对不要 option.series[0].data = newData; // ✅ 应该 chart.setOption({ series: [{ data: newData }] });1
2
3
4
5不要共享option对象
typescript// ❌ 不好 chart1.setOption(sharedOption); chart2.setOption(sharedOption); // ✅ 好 chart1.setOption(_.cloneDeep(option)); chart2.setOption(_.cloneDeep(option));1
2
3
4
5
6
7
🔗 相关资源
下一篇: 忘记-dispose
