【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(三、JavaScript 性能优化1)

【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(课前准备)

【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(一、函数式编程范式)

【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(二、JavaScript 异步编程)

【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(三、手写Promise源码)

【Part1作业】https://gitee.com/zgp-qz/part01-task

【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(一、ECMAScript 新特性)

【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(二、TypeScript 语言)

【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(三、JavaScript 性能优化1)

【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(四、JavaScript 性能优化2)

【Part2作业】https://gitee.com/zgp-qz/part02-homework

课程概述

随着软件不断发展,性能优化是个不可避免的话题,什么样的行为才能算得上是性能优化呢?本质上来说,任何一种提高运行效率,降低运行开销的行为,都可以看作是一种优化操作。这也就意味着在软件开发的过程中有很多值得优化的地方。

特别是在前端应用开发过程中,性能优化可以说是无处不在的。例如:请求资源用到的网络,数据的传输方式,开发过程中所使用到的框架等等,都可以进行优化。
在这里插入图片描述
这里要探讨的是 javascript 本身的优化。

具体来说就是:

从认知内存空间的使用,到垃圾回收的方式介绍,从而让我们可以编写出高效的 javascript 代码。

下面开一下性能优化阶段涉及到的内容:
在这里插入图片描述

内存管理

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

为什么在这里要重提内存管理呢?

下面看个极简单的代码:
在这里插入图片描述

  1. 内存:由可读写的单元组成,表示一片可操作的空间。
  2. 管理:人为的去操作一片空间的申请、使用和释放。
  3. 内存管理:开发者主动申请空间、使用空间、释放空间。
  4. 管理流程:申请 – 使用 – 释放

JavaScript 中的内存管理( JS 中是如何进行内存管理的):

和其他语言是一样的,也是分三步来执行这样一个过程,但是,由于 ECMAScript 中并没有提供相应的操作 API,所以 JS 语言不能像 C 或者 C++ 那样由开发者主动的调用相应的 API 来完成这样的空间管理。

不过,即使如此,它也不能影响我们通过 JS 脚本来演示当前在内部一个空间的生命周期是怎样完成的。
在这里插入图片描述

JavaScript 中的垃圾回收

首先先看一下在 JS 中,什么样的内容会被当做垃圾看待:

  1. JavaScript 当中的内存管理是自动的
    每当我们去创建数字、字符、数组等的时候,它会自动分配相应的空间,后续代码执行的过程中如果通过一些引用关系,无法再找到某些对象的时候,那么这些对象就会被看作是垃圾
  2. 对象不再被引用时是垃圾
    再或者说这些对象其实是已经存在的,但是由于代码中一些不合适的语法或者说结构性的错误,让我们没有办法再去找到的这样的对象,这种对象也会被称作是垃圾
  3. 对象不能从根上访问到时是垃圾

知道了什么是垃圾之后,JS引擎就会出来工作,把它们所占据的空间进行回收,这个过程就是所谓的 JavaScript 垃圾回收。

JavaScript 中的 可达对象

  1. 可以访问到的对象就是可达对象(引用、作用域链)。
  2. 可达的标准就是从根上出发是否能够被找到。
  3. 在 JavaScript 中的根可以理解为当前的全局变量对象(全局执行上下文)。

JavaScript 中的引用与可达

let obj = {
    
    name: 'xm'} // xm 空间被 obj 引用,在全局的执行上下文下, 当前的 obj 可以从根上被找到的(可达的)

let ali = obj // xm 空间又多了一次引用,引用数值变化(后续引用计数算法会用到),

obj = null // obj 到 xm 空间的引用被断掉了,但是 xm 还是可达的,因为 ali 还在引用着

function objGroup(obj1, obj2) {
    
    
    obj1.next = obj2
    obj2.prev = obj1
    return {
    
    
        o1: obj1,
        o2: obj2
    }
}

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

console.log(obj);

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
简单总结说明:

编写代码的时候会存在着一些对象引用的关系,可以从根的下边来进行查找,按照这样一个链条终归能找到某样对象,
但是如果说去找到这样的对象的路径被破坏掉或者说被回收了,这个时候是没有办法再找到它,就会把它视作为垃圾
最后就可以让垃圾回收机制去把它回收掉。
GC算法介绍

GC(Garbage Collection):垃圾回收机制的简写。
GC工作的时候,可以帮助我们找到内存中的垃圾,并释放和回收空间,方便我们后续的代码继续去使用。
在这里插入图片描述
GC 算法是什么?

  1. GC 是一种机制,垃圾回收器完成回收工作
  2. 工作内容就是查找垃圾释放空间,回收空间
  3. 算法就是工作时查找和回收所遵循的规则

常见 GC 算法:

  1. 引用计数
  2. 标记清除
  3. 标记整理
  4. 分代回收
引用计数算法实现原理

核心思想:设置引用数,判断当前引用数是否为 0

在内部去通过一个引用计数器,来维护当前对象的引用数,从而判断该对象的引用数值是否为 0 来决定它是不是一个垃圾对象,当这个数值为 0 的时候 GC 就开始工作,将其所在的空间进行回收释放再使用。

也正式因为引用计数器的存在,导致了引用计数算法在执行效率上与其他的 GC 算法有所差别。

引用计数器 改变的规则:引用关系改变时修改引用数字(当某一个对象它的引用关系发生改变的时候,引用计数器就会主动修改当前这个引用对象所对应的引用数值)

当引用数字为 0 的时候立即回收


const user1 = {
    
     age: 111 }
const user2 = {
    
     age: 222 }
const user3 = {
    
     age: 333 }

const nameList = [user1.age, user2.age, user3.age]

// function fn(){
    
    
//     num1 = 1
//     num2 = 2
// }

function fn() {
    
    
    /** 
     * 加上 const ,num1 和 num2 只能在 fn 内部起效果,当 fn 调用之后,
     * 在外部全局的地方找不到 num1 和 num2 这时候 他们身上的引用计数就是 0 GC 就会立即工作,把它当作垃圾回收掉  
     */
    const num1 = 1
    const num2 = 2
}

fn()
引用计数算法优缺点

优点:

  1. 发现垃圾时立即回收
  2. 最大限度减少程序暂停

缺点:

  1. 无法回收循环引用的对象
  2. 时间开销大
function fn() {
    
    
    const obj1 = {
    
    }
    const obj2 = {
    
    }

    /**
     * 互相的指引关系,就算在外层找不到,里边还在引用着,引用计数算法无法回收
     */
    obj1.name = obj2
    obj2.name = obj1

    return 'this is a test'
}

fn()

标记清除算法实现原理

核心思想:分标记和清除二个阶段完成

第一个阶段:遍历所有的对象,然后所有的活动对象(可达对象),然后进行标记;

第二个阶段:遍历所有的对象,然后把那些身上没有标记的对象进行清除,同时注意的是在第二个阶段当中,它会把第一个阶段所设置的标记给抹掉,便于 GC 下一次还能够正常的工作。

注意一点,标记清除最后去清除的时候当前程序是停止工作的。

这样就能通过两次的遍历行为把当前的垃圾空间进行回收,最终交给一个所谓的空闲列表进行维护,后续可以使用。
在这里插入图片描述
通过 global 往下一直找,所有能找到的都打上标记,而 a1 b1 可能是某一个局部作用域内部成员,这个局部作用域执行完成之后就被回收了,所以从 global 链条下找不到 a1 和 b1 ,这时候 GC 机制就会认为它是垃圾对象,不给它做标记,最终在 GC 工作的时候,就会把 a1 和 b1 回收掉。

标记清除算法优缺点

相对于引用计数来说,标记清除有一个最大的优点:它可以解决之前对象循环引用不能回收的问题(例如上面那张图,如果 a1 和 b1 互相引用,引用计数算法就不管用了)。

缺点:相对于之前的垃圾回收,会产生空间碎片化的问题,不能让空间得到最大化的使用。
在这里插入图片描述

标记整理算法实现原理

首先,标记整理可以看做是标记清除的增强,因为他们在第一个阶段都是一样的,都是遍历所有的活动对象进行标记,只是在清除阶段不同,标记清除是直接将没有标记的垃圾对象做垃圾回收,但是标记整理会在清除之前,先去做一个整理的操作,移动对象的位置,让它们能够在地址上产生连续。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
尽可能最大化的利用所释放出来的内存空间。

常见GC算法总结
  1. 引用计数
  2. 标记清除
  3. 标记整理
认识 V8

V8 是一款主流的 javascript 执行引擎

javascript 之所以能高效的运转,正是因为 V8 的存在

高效,是 V8 的一个最大卖点,速度之所以快,除了背后有一套优秀的管理机制之外,V8 还有一个特点:采用 即时编译

之前很多 javascript 引擎都需要将代码先转换成字节码,然后去执行,而 V8 就可以直接将源码翻译成可以执行的机器码,所以这时候速度是非常快的

还有一个特点:V8 内存设上限
64位 – 1.5G
32位 – 800M

为什么有这么一个操作?

第一:本身是为了浏览器去制造的,所以现有的内存大小对于网页应用来说已经足够使用
第二:V8 内部的垃圾回收机制也决定了它这样的一个设置是非常合理的

官方做过这样一个测试:

当垃圾内存达到 1.5G 的时候,采用增量标记算法进行垃圾回收,只需要消耗 50ms 而如果采用非增量标记去回收,需要 1S,从用户体验来说,1S已经算是很长的时间了,所以这里就以 1.5G 为界,对 V8 内部的内存进行一个上行的设置。

总结:

  1. V8 是一款主流的 javascript 执行引擎
  2. 采用即时编译
  3. V8 内存设上限
V8 垃圾回收策略

在程序的运行中,会用到很多的数据,这些数据又可以分为原始数据和对象类型的数据,对于这些基础的原始数据来说,都是由语言自身来控制的,所以这里所提到的回收主要还是指的是当前存活在我们堆区里的对象数据,因此,这个过程是离不开内存操作的。

而V8当中对内存是做了上限的,所以我们就要知道,在这样的一个情况下,它是怎样对垃圾进行回收的。

  1. 采用分代回收的思想
  2. 内存分为新生代、老生代
  3. 针对不同的对象采用不同的算法

在这里插入图片描述

  1. 分代回收
  2. 空间复制
  3. 标记清除
  4. 标记整理
  5. 标记增量
V8 如何回收新生代对象

在这里插入图片描述
V8 内存分配:

  1. V8 内存空间一分为二
  2. 小空间用于存储新生代对象(32M | 16M)
  3. 新生代指的是存活时间较短的对象(例如:新生代:类似于局部作用域的变量当前程序执行完之后要进行回收,老生代:全局作用域的变量一般都是要整个程序退出之后才进行回收)

新生代对象回收实现:

  1. 回收过程中采用复制算法 + 标记整理
  2. 新生代内存区分为两个等大小空间
  3. 使用空间为 From ,空闲空间为 To
  4. 活动对象存储于From 空间
  5. 标记整理后将活动对象对象拷贝至 To
  6. From 和 To交换空间完成释放

回收细节说明:

  1. 拷贝过程中可能会出现晋升
  2. 晋升指的是将新生代对象移动至老生代
  3. 一轮 GC 之后还存活的新生代需要晋升
  4. To 空间使用率超过 25%
V8 如何回收老生代对象

老生代对象说明:

  1. 老生代对象存放在右侧老生代区域
  2. 64位操作系统1.4G,32位300M
  3. 老生代对象指的是存活时间较长的对象

老生代对象回收实现:

  1. 主要采用标记清除,标记整理,增量标记算法
  2. 首先使用标记清除完成垃圾空间的回收
  3. 采用标记整理进行空间优化
  4. 采用增量标记进行效率优化

细节对比:

  1. 新生代区域垃圾回收使用空间换时间
  2. 老生代区域垃圾回收不适合复制算法

标记增量如何优化垃圾回收?

明确一点:当垃圾回收进行工作的时候,它会阻塞我们 javascript 程序的执行。

所谓的标记增量,简单来说就是将整个垃圾回收操作分成多个小步,组合着去完成当前的整个回收,从而去替代之前一口气做完的垃圾回收操作。

好处就是让程序执行和垃圾回收去交替着完成,而不像以前那样程序运行的时候不做垃圾回收,做垃圾回收的时候不能运行程序。这样带来的时间消耗更加合理一些。
在这里插入图片描述
虽然这样看来,可能让人觉得当前程序停顿了很多次,但是要明白,整个 V8 最大的垃圾回收当它达到 1.5G 的时候采用非增量标记的形式时间也不超过 1秒钟,所以这个间断分割是合理的。而且这样一来呢,就最大限度的把以前的很长一段的停顿时间直接拆分成更小段,这样对于用户来说,就会显得更加得好一些。

V8 垃圾回收总结
  1. V8 是一款主流得 javascript 执行引擎
  2. V8 内存设置上限
  3. V8 采用基于分代回收思想实现垃圾回收
  4. V8 内存分为新生代和老生代
  5. V8 垃圾回收常见得 GC 算法
Performance 工具介绍

为什么使用 Performance 工具?

  1. GC 的目的是为了实现内存空间的良性循环
  2. 良性循环的基石是合理使用
  3. 时刻关注才能确定是否合理
  4. Performance 提供多种监控方式

通过使用 Performance 时刻监控内存

使用步骤:

  1. 打开浏览器输入目标网址
  2. 进入开发人员工具面板,选择 性能(performance)
  3. 开启录制功能,访问具体界面
  4. 执行用户行为,一段时间后停止录制
  5. 分析界面中记录的内存信息

在这里插入图片描述
开启录制
在这里插入图片描述
访问目标网址
在这里插入图片描述
进行用户操作
在这里插入图片描述
点击 stop
在这里插入图片描述
查看生成的分析数据
在这里插入图片描述

内存问题的体现

这里看一下当我们的应用程序在执行的过程中,如果内存出现了问题,那么它具体在界面上如何展示

这里就可以更好的配合 performance 工具进行一个问题的定位,这里就依据一些性能的模型给定的一些判定的标准。

内存问题的外在表现:

  1. 页面出现延迟加载或经常性暂停(网络环境除外)
    一般就判定内存有问题,而且与当前的 GC 存在频繁的垃圾回收是相关的

  2. 页面持续性出现糟糕的性能(网络环境除外)
    一般会认为存在内存膨胀,所谓的内存膨胀指的是当前的界面为了达到最佳的使用速度,它会申请一定的内存空间,但是这个内存空间的大小远超过当前设备本身所能提供的大小。

  3. 页面的性能随着时间延长越来越差
    这个过程一般伴随着内存泄漏
    因为在这种情况下刚开始是没有问题的,可能伴随着某些代码的出现,让我们的内存空间越来越少,这就是所谓的内存泄漏

监控内存的几种方式

当内存出现问题的时候,一般可以归纳为几种情况:

  1. 内存泄漏
  2. 内存膨胀
  3. 频繁的垃圾回收

界定内存问题的标准:

  1. 内存泄漏:内存使用持续升高
  2. 内存膨胀:在多数设备上都存在性能问题
  3. 频繁垃圾回收:通过内存变化图进行分析

监控内存的几种方式:

  1. 浏览器任务管理器
  2. Timeline 时序图记录
  3. 堆快照查找分离 DOM
  4. 判断是否存在频繁的垃圾回收
任务管理器监控内存

demo:
平台:浏览器

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>任务管理器监控内存变化</title>
</head>
<body>
    <button id="btn">Add</button>
    <script>
        btn.onclick = function() {
     
     
            let arrList = new Array(1000000)
        }
    </script>
</body>
</html>

使用浏览器打开:
shift + esc 打开浏览器的任务管理器
在这里插入图片描述
右键将 javascript 内存 列调出来
在这里插入图片描述
在这里插入图片描述
第一列内存指的是 DOM 内存,如果这个内存不断增大,说明页面内部不断的在创建内存
最后一列 javascript 内存,这里表示的是 js 的堆,这一列当中需要关注的是小括号里面的值,它表示的是界面当中,所有可达对象正在使用的内存大小。如果这个数值一直在增大,意味着界面中,要么在创建新对象,要么就是现有对象在不断的增长。

在这里插入图片描述
得出结论:如果说小括号里面的数值一直在增大,意味着当前的内存是有问题的,具体是什么问题,浏览器的任务管理器就看不出来了,只能看出来是有问题的,不能定位问题。

Timeline 记录内存
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>时间线记录内存变化</title>
</head>
<body>
    <button id="btn">Add</button>
    <script>
        const arr = []
        function test() {
     
     
            for(let i = 0; i<100000; i++){
     
     
                document.body.appendChild(document.createElement('p'))
            }
            arr.push(new Array(1000000).join('x'))
        }
        document.getElementById('btn').addEventListener('click',test)
    </script>
</body>
</html>

在这里插入图片描述
打开录制,点击几次按钮,停止录制,生成下方图表
在这里插入图片描述
蓝色:JS堆,红色:文档,绿色:DOM节点,黄色:侦听器,紫色:GPU
在这里插入图片描述
为了便于观察,将除了JS堆之外的全都关掉,便于观察目前这个脚本的 JS 堆 内存的一个走势

这个图叫时序图,鼠标移动到调试工具最上边那一栏图表,以毫秒为单位记录的页面的变化
在这里插入图片描述
可以看到当前页面的一个状态,通过当前页面的状态可以定位到具体某一块代码。

这里只关注 JS 堆内存,可以看到,从刚开始的平稳状态,到内存突然间就上去了,是因为我们的点击按钮操作,而上去之后就又下来了,这是因为浏览器本身的垃圾回收机制,在脚本运行稳定之后 GC 就开始工作了,回收非活动对象,后边几次的上升就是我们的连续点击按钮操作,而中间穿插的下降就是浏览器自己的回收机制,有涨有降。

如果说这个内存走势图是一直往上走,而不往下降,那么这里就存在内存消耗的问题,更有可能是内存泄漏,这时候怎么去定位问题呢?

注意,这里是一个时序图,当我们发现某个节点有问题的时候,可以直接定位到某一个时间节点
在这里插入图片描述
可以直接查看每一个时间点的内存消耗,并可以看到当前时间点界面展示的一个状态,从而定位大致的一个存在问题的代码块。

堆快照查找分离 DOM

工作原理:
找到当前的JS堆,然后进行照片留存,就可以看到里面的所有信息。就像是一个专门针对分离DOM的一种行为。

什么是分离DOM?

界面上看到的很多元素,其实都是DOM 节点,而这些节点本应该存活在DOM树上,不过对于DOM节点会有几种形态:垃圾对象、分离DOM。

如果这个节点从DOM树上脱离,而且在JS代码中也没有人引用着,它其实就成为了一个垃圾,而如果说当前的DOM节点只是从DOM树上脱离了,但是在 JS 代码中还有人在引用着,这种DOM就叫分离DOM。

这种分离DOM 在界面上是看不见的,但是在内存里却占据着空间,所以在这种情况下就是内存泄漏,因此可以通过堆快照的功能把它从这里都找出来,只要能找到,就可以回到代码里面针对它进行清除就行。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>堆快照监控内存</title>
</head>
<body>
    <button id="btn">Add</button>
    <script>
        let tmpEle
        function fn() {
    
    
            let ul = document.createElement('ul')
            for(let i = 0; i < 10; i++){
    
    
                let li = document.createElement('li')
                ul.appendChild(li) // 创建了但是不往页面上创建
            }
            tmpEle = ul
        }
        document.getElementById('btn').addEventListener('click',fn)
    </script>
</body>
</html>

使用浏览器打开这个 html 脚本
内存,时间线上的分配检测,分配采样
两种行为测试:

  1. 没有点击 Add 的情况下获取堆快照
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    发现是没有东西的

  2. 点击 Add 之后再次获取堆快照,并且搜索 detached
    在这里插入图片描述
    你会发现快照2中有内容了,就是JS创建的DOM节点,并没有添加到界面中,但是已经存在了我们的内存当中了,这其实就是一种空间上的浪费,因此这里通过堆快照的功能,找到脚本里边所存在的问题。也就是存在所谓的分离DOM 。

要解决这种问题很简单,在代码中将元素置为 null 就行了,因为之后也不会对其有其他的操作了。
在这里插入图片描述
再次使用浏览器打开并重新获取堆快照查看:
在这里插入图片描述
我们会发现没有了

总结:
利用浏览器提供的一个 堆快照 的功能,把当前的堆进行拍找,拍完照之后查找里边是否存在着一些所谓的分离DOM(detached),因为分离DOM在界面中不体现,但是在内存里边确实存在,它是一种内存的浪费,我们要做的就是定位到代码里边分离DOM所在的位置,然后想办法把它清除掉,例子是直接置为 null。

判断是否存在频繁 GC

为什么确定频繁垃圾回收?

  1. GC 工作的时候应用程序是停止的
  2. 频繁且过长的 GC 会导致应用假死
  3. 用户使用中感知应用卡顿

两种方式:

  1. TimeLine 时序图中内存走势频繁的上升或下降
  2. 任务管理器(浏览器的 shift + esc)中数据频繁的增加/减小
Performance 总结

谷歌浏览器所提供的一款性能工具

  1. 使用流程
  2. 内存问题的相关分析
  3. Performance 时序图监控内存变化
  4. 任务管理器监控内存变化
  5. 堆快照查找分离 DOM
代码优化介绍

如何精准测试 javascript 性能

  • 本质上就是采集大量的执行样本进行数学统计和分析
  • 使用基于 Benchmark.js 的 https://jsperf.com 完成(停止维护了)
    在这里插入图片描述
JSBench 使用

JSBench 一款JSPerf 的替代品,因为 jsperf 网站停止维护。

setup: 前置初始化的一些东西

setup html: 前置的一些HTML元素

setup js: 前置的一些统一的JS代码

teardown: 一些收尾的统一的操作

在这里插入图片描述
在这里插入图片描述
点击 run 之后,等待它运行一段时间
在这里插入图片描述
在这里插入图片描述
看这里就能看出来两段同样代码的效率了

建议:

使用性能测试的时候开一个标签页,其他的都关掉,因为浏览器是多线程的,随着打开的标签页越来越多,会互相之间抢占当前的资源。

再有就是测试的时候尽可能的停在当前的页面,因为这里可能会有一个挂起的操作。操作系统是可以同时做多件事情的,而测试过程中代码的执行是消耗时间的,因为它要大量的取样,如果这时候把界面最小化或者关掉,然后去写其他代码,在这个过程中它有可能会被挂起,再回来的时候这个时间不一定是准确的。

再有就是常规的需要注意的点,不能就执行一次得到的结论跟我们想的一样或者说不一样就认为它是合理的或者说它是最终的答案,我们应该让脚本多执行几次,然后取那个几率更高的一个结果,可能就是我们想要的测试行为。

再有就是在程序的执行中,不应该纠结于代码的执行时间,当前的工具主要的是跑一下代码的执行速度,但是对于我们的性能测试,并不是关注的只有时间,它只是众多性能指标中的一个。

一段代码执行速度快,并不意味着这段代码就很健壮,关注的点不一样,那么衡量的标准和结果就不同了,而对于代码中所涉及到的性能,其实就两个方面:要么拿空间换时间,要么节省更多的空间,换取时间。

慎用全局变量

从字面上看,就是说程序执行过程中,如果针对于某些数据需要进行存储,我们需要尽可能的把它放置在局部作用域当中变成一个局部变量,至于说为什么要这么做?我们且看分析:

为什么要慎用?

  • 全局变量定义在全局执行上下文,是所有作用域的顶端
    js 查找是按照层级往上查找的,下边局部作用域的变量没有找到,最终都会去查到最顶端的全局上下文,这种情况下,查找的时间消耗是非常大的,降低了代码的执行效率,
  • 全局执行上下文一直存在于上下文执行栈,直到程序退出
    所以这种情况下对于 GC 的工作是非常不利的,不会把它当作垃圾对象进行回收
  • 如果某个局部作用域出现了同名变量,则会遮蔽或污染全局

总归来说,我们在使用全局变量的时候,就需要考虑更多的事情,否则,就会给我们带来一些意想不到的情况。

这里只讨论执行效率。

// 全局变量
var i, str = ''

for(i = 0; i < 1000; i++){
    
    
    str += i
}


// 局部变量
for(let i = 0; i < 1000; i++){
    
    
    let str = ''
    str += i
}

在这里插入图片描述
在这里插入图片描述

缓存全局变量

通过缓存全局变量的方式,让代码有更高的执行性能

将使用中,无法避免的 全局变量,缓存到局部

这里对比一下采用局部缓存和不采用局部缓存,性能差异到底有多大:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>缓存全局变量</title>
</head>
<body>
    <input type="button" value="btn" id="btn1">
    <input type="button" value="btn" id="btn2">
    <input type="button" value="btn" id="btn3">
    <input type="button" value="btn" id="btn4">
    <p>111</p>
    <input type="button" value="btn" id="btn5">
    <input type="button" value="btn" id="btn6">
    <input type="button" value="btn" id="btn7">
    <p>222</p>
    <input type="button" value="btn" id="btn8">
    <input type="button" value="btn" id="btn9">
    <p>333</p>
    <input type="button" value="btn" id="btn10">
    <script>
        // 没做缓存
        function getBtn1(){
     
     
            let oBtn1 = document.getElementById('btn1')
            let oBtn3 = document.getElementById('btn3')
            let oBtn5 = document.getElementById('btn5')
            let oBtn7 = document.getElementById('btn7')
            let oBtn9 = document.getElementById('btn9')
            let oBtn10 = document.getElementById('btn10')
        }
        // 做缓存
        function getBtn2(){
     
     
            let doc = document
            let oBtn1 = doc.getElementById('btn1')
            let oBtn3 = doc.getElementById('btn3')
            let oBtn5 = doc.getElementById('btn5')
            let oBtn7 = doc.getElementById('btn7')
            let oBtn9 = doc.getElementById('btn9')
            let oBtn10 = doc.getElementById('btn10')
        }
    </script>
</body>
</html>

在这里插入图片描述
在这里插入图片描述
可以看到,做缓存后的代码会稍微提高运行效率

通过原型对象添加附加方法

js 中的三种概念:

构造函数、原型对象、实例对象,构造函数和实例对象都是可以指向原型对象。

如果某个构造函数的内部有一个成员方法,而后续的实例对象都需要频繁的去调用,这里就可以直接将它添加在原型对象上,而不需要把它放在构造函数内部。

这两种不同的实现方式,在性能上有所差异。

  • 在原型对象上新增实例对象需要的方法

这里测试一下将代码放在构造函数内部和放在原型对象上的性能差异:


var fn1 = function() {
    
    
  this.foo = function() {
    
    
    console.log(11111)
  }
}

let f1 = new fn1()


var fn2 = function() {
    
    }
fn2.prototype.foo = function() {
    
    
  console.log(11111)
}

let f2 = new fn2()

在这里插入图片描述
可以看到,运行效率差别还是很大的

避开闭包陷阱

闭包的特点:

外部具有指向内部的引用,
在“外”部作用域访问“内”部作用域的数据
在这里插入图片描述
关于闭包:

  • 闭包是一种强大的语法
  • 闭包使用不当容易造成内存泄漏
  • 不要为了闭包而闭包
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>闭包陷阱</title>
</head>
<body>
    <button id="btn">add</button>
    <script>
        function foo() {
     
     
            var el = document.getElementById('btn')
            el.onclick = function() {
     
     
                console.log(el.id)
            }
        }
        foo()
    </script>
</body>
</html>

可以看到,onclick 函数引用到了foo作用域的变量,这里边的 el 是一直被引用着的,无法被回收,当这种代码越来越多的时候,对于内存是非常不友好的,也就是闭包所存在的陷阱,就是内存泄漏。

处理:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>闭包陷阱</title>
</head>
<body>
    <button id="btn">add</button>
    <script>
        function foo() {
     
     
            var el = document.getElementById('btn')
            el.onclick = function() {
     
     
                console.log(el.id)
            }
            el = null // 清掉之后 GC 会自动清除这块内存空间
        }
        foo()
    </script>
</body>
</html>
避免属性访问方法使用

关于属性访问方法,是和面向对象相关的,为了实现更好的封装性,所以更多的时候可能会将一些成员属性和方法给放在一个函数的内部,然后在外部去暴漏这样的一个方法对当前的属性进行增删改查的操作,但是在 js 的面向对象当中并不是特别的适用。

因为

  • 在 js 里面是不需要属性的访问方法的,所有的属性都是外部可见的
  • 在使用属性访问方法的时候相当于增加了一层重定义,没有访问控制力
function Person() {
    
    
  this.name = 'icoder'
  this.age = 18
  this.getAge = function() {
    
    
    return this.age
  }
}

const p1 = new Person()
const a = p1.getAge()



function Person() {
    
    
  this.name = 'icoder'
  this.age = 18
}
const p2 = new Person()
const b = p2.age

在这里插入图片描述

For 循环优化

for 循环是编码过程中经常使用到的语法结构,每当遇到数组或者说类数组结构都可以采用for循环的方式遍历。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>For循环优化</title>
</head>
<body>
    <p class="btn">add</p>
    <p class="btn">add</p>
    <p class="btn">add</p>
    <p class="btn">add</p>
    <p class="btn">add</p>
    <p class="btn">add</p>
    <p class="btn">add</p>
    <p class="btn">add</p>
    <p class="btn">add</p>
    <p class="btn">add</p>
    <script>
        var aBtns = document.getElementsByClassName('btn')
        for(var i = 0; i < aBtns.length; i ++){
     
     
            console.log(i)
        }

        for(var i = 0, len = aBtns.length; i < len; i ++){
     
     
            console.log(i)
        }
    </script>
</body>
</html>

在这里插入图片描述
可以看到速度是稍微快一些的

选择最优的循环方法

在实际开发应用中,往往会遇到大量数据的遍历结构,而拿到这样结构之后,往往又有多种选择进行遍历,例如:for ,forEach ,for … in 等等。

这里比对一下同样的数据,不同的遍历方法,哪个会相对快一些。

var arrList = new Array(1, 2, 3, 4, 5)

arrList.forEach(function(item) {
    
    
  console.log(item)
})

for (var i = arrList.length; i; i--) {
    
    
  console.log(arrList[i])
}

for (var i in arrList) {
    
    
  console.log(arrList[i])
}

在这里插入图片描述
可以看到,forEach 是最快的,for … in 是最慢的

文档碎片优化节点添加

针对于外部应用开发来说,DOM 操作是非常频繁的,而针对于 DOM 的交互操作,又是非常消耗性能的,特别是创建新的节点,将节点添加至界面中时,这个过程一般都会伴随着回流和重绘,这两个操作对于性能的消耗时比较大的,这里看一下针对于节点的添加有怎样的优化操作:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>优化节点添加</title>
</head>
<body>
    <script>
        // 直接插入
        for(var i = 0; i < 10; i++){
     
     
            var op = document.createElement('p')
            op.innerHTML = i
            document.body.appendChild(op)
        }

        // 先创建文档碎片,再插入
        const fragEl = document.createDocumentFragment()
        for(var i = 0; i < 10; i++){
     
     
            var op = document.createElement('p')
            op.innerHTML = i
            fragEl.appendChild(op)
        }
        document.body.appendChild(fragEl)
    </script>
</body>
</html>

在这里插入图片描述
通过数字可以看出来通过文档碎片的方式统一添加要比直接 create append 的操作是要快的。

克隆优化节点操作

所谓的克隆指的是:当我们要去新增节点的时候,可以找到一个与他类似的一个节点,把它克隆一下,然后再把克隆好的这样的节点直接添加到我们界面当中。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>克隆优化节点操作</title>
</head>
<body>
    <p id="box">old</p>
    <script>
        // 新建
        for(var i = 0; i < 3; i++){
     
     
            var op = document.createElement('p')
            op.innerHTML = i
            document.body.appendChild(op)
        }

        // 克隆
        var oldP = document.getElementById('box')
        for(var i = 0; i < 3; i++){
     
     
            var op = oldP.cloneNode(false)
            op.innerHTML = i
            document.body.appendChild(op)
        }
    </script>
</body>
</html>

在这里插入图片描述
通过测试得出,克隆的效率要高于新建的效率

直接量替换 new Object

所谓的直接量替换 new Object 就是当我们要创建数组的时候,我们可以直接用 new 的方式创建,也可以直接采用它的字面量。

// 字面量
var a = [1, 2, 3]

// new Object 
var a1 = new Array(3)
a1[0] = 1
a1[1] = 2
a1[2] = 3

在这里插入图片描述
可以看出来通过字面量直接创建的这种方式相对效率要更高一点

【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(课前准备)

【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(一、函数式编程范式)

【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(二、JavaScript 异步编程)

【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(三、手写Promise源码)

【Part1作业】https://gitee.com/zgp-qz/part01-task

【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(一、ECMAScript 新特性)

【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(二、TypeScript 语言)

【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(三、JavaScript 性能优化1)

【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(四、JavaScript 性能优化2)

【Part2作业】https://gitee.com/zgp-qz/part02-homework

猜你喜欢

转载自blog.csdn.net/qq_38652871/article/details/108871751