概述

我们都知道,随着软件开发行业的不断发展,性能优化已经是一个不可避免的话题, 那么什么样的行为才能算得上是性能优化呢?

本质上来说任何一种可以提高运行效率,降低运行开销的行为,我们都可以看做是一种优化操作。

这也就意味着,在软件开放行业必然存在着很多值得优化的地方,特别是在前端开发过程中,性能优化我们可以认为是无处不在的。例如请求资源时所用到的网络,以及数据的传输方式,再或者开放过程中所使用到的框架等他们都可以去进行优化。

而本阶段我们要探索的是JavaScript语言本身的优化,具体来说就是从认知内存空间的使用到垃圾回收的方式介绍,从而让我们可以编写出高效的javascript代码。

那么下面我们就来看一下在性能优化阶段,我们会涉及到哪些内容。

首先第一个部分我们会遇到内存管理,在这里呢我们首先会说明为什么我们的内存是需要管理的,以及内存管理的基本流程。

同事我们也会去介绍一些常见GC的算法,让你可以灵活的去应对一些所谓的大场面试。

在这之后呢,我们还会介绍当前市面上非常流行的V8引擎,那么我们会去具体介绍V8引擎当中使用的是什么样的GC算法,在实现我们当前的垃圾回收。

那么接下来当我们了解了内存的管理,以及垃圾回收的一些相关操作之后,我们就会通过谷歌浏览器所提供的一些工具,例如Performance来具体的演示一下,如何对内存进行监控,从而去发现我们当前的代码中是否存在着可以优化的性能空间。

最后我们会去通过一些代码示例来作为演示,让我们可以具体的去完成一些代码层面上的优化操作。

那这块就是针对于性能优化相关的一些内容介绍。

内存管理

这里我们来看一下Javascript的内存管理相关的内容。

随着近些年硬件技术的不断发展,同时高级变成语言当中都自带了GC机制,所以这样一些变化就让我们在不需要特别注意内存空间使用的情况下,也能够正常的去完成相应的功能开发。

那么为什么在这里我们一定要去重提内存管理呢,下面我们就通过一段极简单的代码来进行说明。

首先我们在这样一个地方定义了一个普通的函数fn, 然后我们再函数体内去声明了一个数组,紧接着我们去给数组赋值,需要注意的是我们在赋值的时候刻意选择了一个比较大的数字来作为下标。

这样做的目的呢也就是为了当前这个函数在调用的时候可以像我们的内存尽可能多的申请一片比较大的空间。

function fn() {
    arrlist = []
    arrlist[100000] = 'this is a lg'
}

fn()

然后我们在执行这个函数过程中其实从语法上来说他是不存在任何问题的,不过当我们用相应的一个性能监控工具来在这个脚本执行过程中对他的内存进行监控的时候,我们会发现,那么他的内存变化是持续程线性升高的,那么在这个过程当中没有回落。

那么这代表着什么呢?非常简单,这就是一个内存泄露,至于说他如何泄露呢,我们现在不需要纠结。

我们只是想在这去声明,如果说我们在写代码的时候不够了解内存管理的一个机智。那么从而呢就会让我们去编写出一些不容易察觉到的内存问题性代码。

那么像这种代码多了以后呢那么给我们程序带来的可能就是一些意想不到的bug,所以掌握内存的管理,还是非常有必要的。

因此接下来呢我们就去看一下,什么是内存管理。

从我们当前这样一个词语本身来说呢,内存其实就是由可读写的单元组成,他就标识一片可操作的空间。

而管理呢,在这里我们想刻意强调的就是由人主动去操作这片空间的申请,使用和释放,即使我们借助了一些API,但终归来说我们可以自主的来做这样一个事情。

所以内存管理呢我们就认为是,开发者可以主动的向内存来申请空间,使用空间,并且呢去释放空间。

因此这个流程呢,也就显得非常简单了,一共呢就三步,申请,使用和释放。

那说完了这些以后,我们就回到我们的JavaScript当中,我们来看一下在JavaScript里面,他是如何来完成内存管理的。

其实和其他的语言是一样的,他也是分三步来执行这样一个过程,但是呢由于ECMAScript当中并没有提供相应的操作API。

所以JS预发不能像C或者C++那样,由开发者主动去调用相应的API来完成这样一个空间的管理。

不过即使如此他也不能影响我们去通过js脚本来演示当前在内部一个空间的生命周期是怎样完成的。

所以下边呢我们就回到编辑器当中,来通过一段简单的JS脚本演示一下我们当前这样一个内存空间的生命周期是什么样子的。

那这一块我们就先回到编辑器当中,在这里我们就通过一段简短的代码去把刚才我们所看到的流程在我们的JS当中呢去进行一个实现。

首先我们把这个顺序先简单的写一下,第一个我们要去申请空间,第二个我们要去使用空间,第三个我们要去释放空间。

那在我们JS当中由于我们并没有直接提供相应的API,所以我们只能是在JS执行引擎去遇到变量定义语句的时候自动分配给我们一个相应的空间。

所以这个步骤呢我们就相当于去定义一个变量,比如在这我们采用let定义一个obj,然后我们把它指向一个空对象。

然后紧接着对她的使用呢其实就是一个读写的操作,所以这个时候我们可以直接往我们这个对象里面写入一个具体的数据就可以了。我们写上一个yd。

完成这个操作以后我们最后可以对她进行释放,同样的我们js里面并没有相应的释放API,所以我们在这里可以采用一种间接的方式,比如我们去找到我们的obj然后直接去把他设置为null。

let obj = {}

obj.name = 'yd'

obj = null

那这个时候就相当于是我们按照内存管理的一个流程在js当中去实现了这样一个内存管理。

那这里我们就保存,看一下当前代码其实也没有任何的提示,后期我们会在这样一个性能监控工具当中去看一下这样的一个走势就可以了。

那这里就是关于js当中内存管理相应的一些内容介绍。

垃圾回收

接下来我们来看一下JavaScript中的垃圾回收,首先我们来看一下在JavaScript当中什么样的内容会被当中是垃圾看待。在我们的后续的GC算法当中,他也会存在的一个垃圾的概念,两者呢其实是完全一样的。所以在这里我们就统一的说明。

首先对于我们前端开发来说呢JavaScript当中的内存管理是自动的。每当我们去创建一个对象、数组或者函数的时候呢,他就会自动的去分配相应的内存空间。

然后后续程序代码在执行的过程中如果通过一些引用关系无法再找到某些对象的时候那么这些个对象就会被看作是垃圾。

那再或者说我们这些对象其实已经存在的,但是由于我们代码当中一些不合适的语法或者说结构性的错误,让我们没有办法再去找到这样的一个对象。那么这种对象也会被称之是垃圾。

那知道了什么是垃圾之后这个JavaScript执行引擎就会出来工作,然后把他们所占据的对象空间进行回收,那这个过程呢就是我们所谓的JavaScript垃圾回收。

说完这个以后呢我们在这里用到了几个小的概念,第一是引用,第二是从根上访问,而这个操作呢在后续的GC里面也会被频繁的使用到。

所以说我们在这再去说一个名词叫可达对象,首先在JavaScript当中可达对象理解起来非常的容易,就是我们能访问到的对象就是可达。

那至于说怎么访问呢,我们可以通过具体的引用也可以在当前的上下文当中去通过作用域链来进行查找。

只要我们能找得到,我们就认为呢是可达的。不过这里边会有一个小的标准限制就是我们一定要是从根上出发找得到才认为他是可达的。

所以说在这我们又要去讨论一下什么是根呢,在JavaScript里面我们可以认为当前的全局变量对象就是我们的根,也就是我们所谓的全局执行上下文。

那说完了这些以后我们简单的总结一下就是JavaScript当中的垃圾回收其实就是找到垃圾,然后让JavaScript的执行引擎来进行一个空间的释放和回收。

那么在这里我们用到了引用和可达对象所以接下来我们就尽可能的去通过代码的方式来看一下,在js中的引用与可达是怎么体现的。

那么说完这些以后我们就先回到代码编辑器当中,在这里我们通过一些代码片段的演示来说明一下我们当前的引用和可达他们的具体操作。

在这我们首先定义一个变量,为了后续我们可以修改值我们采用let关键字,我们定一个obj让他指向一个对象。

为了方便描述这样一个对象空间我就直接给他起一个名字叫xiaoming,简单的去约定一下我会直接把当前这样一个对象空间称之为小明的空间了。

let obj = {name: 'xiaoming'}

那么写完这行代码以后呢其实就相当于是这个空间被我当前的obj对象引用了,那这里就出现了引用。

再者来说站在我们全局的一个执行上下文下我们当前这样一个obj是可以从根上来被找到的。

所以说这个obj他也是一个可达的,这也就间接地意味着我们当前xiaoming的对象空间呢其实就是一个可达的。

那说完这些以后我们在做一些操作,比如在这个地方我们重新再去定义一个变量,比如ali,让他等于obj,那做完这一步操作以后呢我们就可以认为小明的空间呢又多了一次引用。

let obj = {name: 'xiaoming'}

let ali = obj

所以说在这里我们是存在着一个引用数值变化的,那这个概念在我们后续的引用计数算法中是会用到的。

那么说完这个以后我们再来做一个事情,我直接找到obj然后把它重新赋值为null。那么这个操作做完之后我们就可以思考一下了。

本身来说呢小明这样的一个对象空间是有两个引用的。而随着我们null赋值代码的执行呢,我们obj到我们小明空间的引用呢就相当于是被切断了。

那么现在我们当前这样一个小明对象是否还是可达呢?必然是的。

因为我们ali还在引用着这样的一个对象空间,所以说他依然是一个可达对象。

那这块就是一个引用的主要说明,顺带呢我们也看到了一个可达。

那么接下来我们再去举一个示例,来说明一下我们当前js当中的可达操作,不过这里面我们需要提前说明一下。

为了方便我们后面的演示GC当中的标记清除算法,所以这个实例我们会稍微写的麻烦一些。

首先在这里我们先定义一个函数,我们起个名字叫objGroup,在这里我们给他设置两个形参,一个叫obj1,一个叫obj2,接下来在他里面我们去做一些事情。

首先我们让obj1通过一个属性然后指向obj2,然后紧接着我们再让obj2也通过一个属性去指向obj1。

我们再通过return关键字直接去返回一些内容,而这个内容我们给出的还是对象,obj1通过o1进行返回,我们再去设置一个o2让他去找到obj2。

完成之后我们在外部去调用这样一个函数,这里我们设置一个变量进行接收,我们叫obj等于我们当前objGroup调用的结果。

我们给他们传两个参数,分别是两个对象,obj1和obj2。

function objGroup(obj1, obj2) {
    obj1.next = obj2;
    obj2.prev = obj1;
}

let obj = objGroup({name: 'obj1'}, {name: 'obj2'});

console.log(obj);

我们运行看一下打印出来额是什么, 我们可以发现得到了一个对象

{
    o1: {name: 'obj1', next: {name: 'obj2', prev: [Circular]}},
    o2: {name: 'obj2', next: {name: 'obj1', prev: [Circular]}}
}

那么这个对象里面分别有obj1和obj2,而obj1和obj2他们内部又各自通过一个属性去指向了彼此。

我们来分析一下我们编写的代码,首先从全局的根去出发,我们这块是可以找到一个可达的对象obj,那么他是通过一个函数调用之后指向了一个内存空间,他的里面就是我们上面看到的o1和o2。

然后在o1和o2的里面呢我们刚好又通过相应的属性去指向了一个obj1的空间和obj2的空间。

对于obj1和obj2来说呢,他俩之间又通过next和prev做了一个互相的一个引用,所以现在这样一个过程来说我们代码里面所出现的那些对象其实都可以从根上来进行查找。

不论我们找起来是多么的麻烦,总之来说呢这块我们都能够找得到的,那么继续往下来呢我们再来做一些分析。

如果说我们现在代码里面去做一件事情,现在我们去通过一个delete语句把我们obj身上这样一个o1的引用以及我们obj2对我们当前obj1的引用呢都给他直接delete掉。

那此时此刻就说明了我们现在是没有办法再直接通过什么样的方式来找到obj1这样的一个对象空间的,也就是说他会变成我们直接把所有能找到obj1的路径都给删除了,那么在这里他就会被认为是一个垃圾的操作。

那么最后,我们的js引擎呢就会去找到他,然后对其进行回收。

我们这里说的比较麻烦,那么简单来说就是我们当前在编写代码的时候会存在的一些对象引用的关系,然后我们可以从根的下边呢,来进行查找,按照引用关系我们终究能找到一些对象。

但是如果说我们去找到这些对象的一些路径呢,被破坏掉或者说被回收了,那么这个时候我们是没有办法再找到他,就会把他视作是垃圾,最后我们就可以呢,让垃圾回收机制呢,去把他回收掉。

那,关于这块呢,就是我们当前,js当中的垃圾回收以及引用和对象可达相关的一些介绍内容。