From 7f23a4c0c13f283cc9ff3bca266744edfad01706 Mon Sep 17 00:00:00 2001 From: hustcc Date: Thu, 26 Mar 2020 14:35:22 +0800 Subject: [PATCH] fix(#2222): fix scale pool memory leak --- src/chart/util/scale-pool.ts | 27 ++++++++++++++ src/chart/view.ts | 29 ++++++++++----- tests/bugs/2222-spec.ts | 72 ++++++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 10 deletions(-) create mode 100644 tests/bugs/2222-spec.ts diff --git a/src/chart/util/scale-pool.ts b/src/chart/util/scale-pool.ts index 4e68d18ea0..3ab1d2c5fb 100644 --- a/src/chart/util/scale-pool.ts +++ b/src/chart/util/scale-pool.ts @@ -11,6 +11,7 @@ interface ScaleMeta { readonly key: string; readonly scale: Scale; scaleDef: ScaleOption; + syncKey?: string; } /** @ignore */ @@ -123,6 +124,7 @@ export class ScalePool { // 2. 缓存到 syncScales,构造 Record 数据结构 const syncKey = this.getSyncKey(sm); + sm.syncKey = syncKey; // 设置 sync 同步的 key // 因为存在更新 scale 机制,所以在缓存之前,先从原 syncScales 中去除 sync 的缓存引用 this.removeFromSyncScales(key); @@ -155,6 +157,31 @@ export class ScalePool { return scaleMeta && scaleMeta.scale; } + /** + * 在 view 销毁的时候,删除 scale 实例,防止内存泄露 + * @param key + */ + public deleteScale(key: string) { + let scaleMeta = this.getScaleMeta(key); + if (scaleMeta) { + const { syncKey } = scaleMeta; + + const scaleKeys = this.syncScales.get(syncKey); + + // 移除同步的关系 + if (scaleKeys && scaleKeys.length) { + const idx = scaleKeys.indexOf(key); + + if (idx !== -1) { + scaleKeys.splice(idx, 1); + } + } + } + + // 删除 scale 实例 + this.scales.delete(key); + } + /** * 清空 */ diff --git a/src/chart/view.ts b/src/chart/view.ts index 7dba142aaf..c5900950b2 100644 --- a/src/chart/view.ts +++ b/src/chart/view.ts @@ -136,6 +136,8 @@ export class View extends Base { private isPreMouseInPlot: boolean = false; /** 默认标识位,用于判定数据是否更新 */ private isDataChanged: boolean = false; + /** 从当前这个 view 创建的 scale key */ + private createdScaleKeys = new Map(); constructor(props: ViewCfg) { super({ visible: props.visible }); @@ -227,7 +229,6 @@ export class View extends Base { public clear() { this.emit(VIEW_LIFE_CIRCLE.BEFORE_CLEAR); // 1. 清空缓存和计算数据 - this.scalePool.clear(); this.filteredData = []; this.coordinateInstance = undefined; this.isDataChanged = false; // 复位 @@ -243,6 +244,12 @@ export class View extends Base { controller.clear(); }); + // 4. 删除 scale 缓存 + this.createdScaleKeys.forEach((v: boolean, k: string) => { + this.getRootView().scalePool.deleteScale(k); + }); + this.createdScaleKeys.clear(); + // 递归处理子 view each(this.views, (view: View) => { view.clear(); @@ -1215,21 +1222,18 @@ export class View extends Base { * @param scaleDef * @param key */ - protected createScale(field: string, data: Data, scaleDef: ScaleOption, key?: string): Scale { + protected createScale(field: string, data: Data, scaleDef: ScaleOption, key: string): Scale { // 1. 合并 field 对应的 scaleDef,合并原则是底层覆盖顶层(就近原则) const currentScaleDef = get(this.options.scales, [field]); const mergedScaleDef = { ...currentScaleDef, ...scaleDef }; - // 2. 生成默认的 key - const defaultKey = key ? key : this.getScaleKey(field); - - // 3. 是否存在父 view,在则递归,否则创建 + // 2. 是否存在父 view,在则递归,否则创建 if (this.parent) { - return this.parent.createScale(field, data, mergedScaleDef, defaultKey); + return this.parent.createScale(field, data, mergedScaleDef, key); } - // 4. 在根节点 view 通过 scalePool 创建 - return this.scalePool.createScale(field, data, mergedScaleDef, defaultKey); + // 3. 在根节点 view 通过 scalePool 创建 + return this.scalePool.createScale(field, data, mergedScaleDef, key); } /** @@ -1482,12 +1486,17 @@ export class View extends Base { const scaleDef = get(scales, [field]); // 调用方法,递归去创建 + const key = this.getScaleKey(field); this.createScale( field, // 分组字段的 scale 使用未过滤的数据创建 groupedFields.includes(field) ? data : filteredData, - scaleDef + scaleDef, + key, ); + + // 缓存从当前 view 创建的 scale key + this.createdScaleKeys.set(key, true); }); } diff --git a/tests/bugs/2222-spec.ts b/tests/bugs/2222-spec.ts new file mode 100644 index 0000000000..d73891f1ec --- /dev/null +++ b/tests/bugs/2222-spec.ts @@ -0,0 +1,72 @@ +import { Chart } from '../../src'; +import { createDiv } from '../util/dom'; + +describe('#2222', () => { + const div = createDiv(); + div.style.height = '400px'; + + const data = [ + { year: '1991', v1: 3, v2: 6 }, + { year: '1992', v1: 4, v2: 8 }, + ]; + const chart = new Chart({ + container: div, + autoFit: true, + height: 200, + }); + + const v1 = chart.createView(); + v1.data(data); + v1.point().position('year*v1'); + + const v2 = chart.createView(); + v2.data(data); + v2.line().position('year*v2'); + + chart.scale({ + v1: { sync: 'value' }, + v2: { sync: 'value' }, + }); + + chart.render(); + + it('scale pool should be clear when remove view', () => { + // @ts-ignore + expect(chart.scalePool.scales.size).toBe(4); + // @ts-ignore + expect(chart.scalePool.syncScales.get('value').length).toBe(2); + + // 重新渲染 + chart.render(); + // @ts-ignore + expect(chart.scalePool.scales.size).toBe(4); + // @ts-ignore + expect(chart.scalePool.syncScales.get('value').length).toBe(2); + + // 删除 + chart.removeView(v1); + // @ts-ignore + expect(chart.scalePool.scales.size).toBe(2); + // @ts-ignore + expect(chart.scalePool.syncScales.get('value').length).toBe(1); + + const v3 = chart.createView(); + v3.data(data); + v3.point().position('year*v1'); + + chart.render(); + // @ts-ignore + expect(chart.scalePool.scales.size).toBe(4); + // @ts-ignore + expect(chart.scalePool.syncScales.get('value').length).toBe(2); + }); + + it('scale pool should be clear when clear', () => { + chart.clear(); + + // @ts-ignore + expect(chart.scalePool.scales.size).toBe(0); + // @ts-ignore + expect(chart.scalePool.syncScales.get('value').length).toBe(0); + }) +});