Lucas Liao's Blog

如何理解js中垃圾回收

前言

JavaScript中,通常我们通过以下方式创建变量:

1
2
3
4
5
6
let v = 1;
let obj = {
a: 1,
b: 2
}
let array = new Array(3).fill(0)

数据变量分为两大类型,基本类型以及引用类型,基本类型包括String,Boolean,Symbol,Number,null, undefined, BigInt这七大种,而引用类型通常是我们所说的Object,包括数组,对象,函数等。

我们在创建变量,其实就是分配内存的过程,JavaScript自带垃圾回收机制,通过内部的机制回收内存,然而js内部是通过怎样的方式判断可以进行垃圾回收了呢?dangdang~~ 那就是变量的引用。

这里主要介绍垃圾回收算法,垃圾回收算法主要依赖于引用的概念,在内存管理的环境,一个对象如果有访问另一个对象的权限(隐式或显式),叫做一个对象引用另外一个对象。

在这里,“对象”的概念不仅特指普通对象,还包括函数作用域。

对象js的内存管理原理,我就不班门弄斧介绍一遍了,详细请看MDN的介绍,Memory Management

正文

下面分享下以前老东家项目的处理方案,在复杂的业务场景,如何进行内存释放。简单来说,就是如何设计一种比较合理的方式,清除不再使用的变量数据。复杂的业务功能,伴随着复杂的数据类型以及结构,如果不手动进行清除,很可能导致内存占用过多,造成程序崩溃。本方案基于面向对象编程思维,每个业务模块通过定义class,创建instance实例维护数据及方法。

参考了当时源哥的写法,源哥是我当时的大佬,repest!有机会专门写一篇源哥的我的影响,源哥的github

既然是通过面向对象,在实际的项目中,会通过new className这种方式创建不同业务的实例,因此,本方案的做法是定义一个基础类GCBaseService,把内存释放逻辑封装起来,各个模块类型通过extends的方式继承基础类。

说得好像比较模糊,show me code…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export class GCBaseService {
// protected gcService: GCService;

public readonly destroy: Function;

constructor() {
// this.gcService = new GCService();
this.destroy = this._destroy.bind(this);
}

private _destroy() {
// if (this.gcService) {
// this.gcService.gc();
// }

this.onDestroy();
}

protected onDestroy() {
// please override me
}
}

以上是我们定义好的基本class,如何使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

class demo extends GCBaseService {
constructor() {
// 这里我们会创建一些数据
this.v1 = [];
this.v2 = {};
}

onDestroy() {
this.v1 = null;
this.v2 = null;
}
}

调用方式:

1
2
let demo = new Demo();
demo.destroy()

当调用了实例当destroy()方法,其实就是触发了onDestroy()的逻辑。这里你可能有疑问,为什么需要通过这种方式,多此一举呢?

其实这里有个拓展的场景,把所有变量清除的工作放在onDestroy()总感觉比较受限,于是,引入RxJS的概念,上面你也发现我注释了部分代码,完整版如下,我们需要再定义一个
回收订阅类GCService。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default class GCService {
private readonly _gc$: BehaviorSubject<boolean>;
public readonly gc$: Observable<undefined>;

constructor() {
this._gc$ = new BehaviorSubject<boolean>(false);
this.gc$ = this._gc$.asObservable().filter(destroyed => destroyed === true).take(1).map(() => undefined);
}

public get destroyed(): boolean {
return this._gc$.getValue() === true;
}

public gc() {
if (this.destroyed === true) return;

this._gc$.next(true);
this._gc$.complete();
}
}

在constructor里面,我们采用take(1)的方式,保证发布垃圾回收只会触发一次。

回到最初的创建的demo类,如果你的项目刚好也是采用RxJs管理数据流的,可以通过源流定义takeUntil, 传入this.gcService.gc$,就是可以在destory的时候控制停止接受数据,使得这个回收机制的设计更加丰富。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class demo extends GCBaseService {
constructor() {
// 这里我们会创建一些数据
this.v1 = [];
this.v2 = {};

this.rxStream$.takeUntil(this.gcService.gc$).subscribe(res => {
// to update
})
}

onDestory() {
this.v1 = null;
this.v2 = null;
}
}

总结:

讲道理,本套方案的设计并不复杂,亮点在于形成了一种编码规范,把一些琐碎而又不可忽视的逻辑封装了起来,让团队成员很舒服地用起来,在多人开发中,更容易回溯项目代码,而且这样设计,不是很优雅嘛?溜了溜了

完。