文章目录
- JS性能优化
- JS内存管理
- JS中的垃圾回收
- GC算法介绍
- 引用计数算法实现原理
- 引用计数算法优缺点
- 标记清除算法实现原理
- 标记清除算法优缺点
- 标记整理算法实现原理
- 常见GC算法总结
- 认识V8
- V8垃圾回收策略
- V8如何回收新生代对象
- V8如何回收老生代对象
- V8垃圾回收总结
- Performance工具介绍
- 内存问题的体现
- 监控内存
- 任务管理器监控内存
- Timeline记录内存
- 堆快照查找分离DOM
- 判断是否存在频繁GC
- Performance总结
- 代码优化介绍
- 慎用全局变量
- 缓存全局变量
- 通过原型对象添加附加方法
- 避开闭包陷阱
- 避免属性访问方法使用
- For循环优化
- 选择最优的循环方法
- 文档碎片优化节点添加
- 克隆优化节点操作
- 直接量替换new Object
- JSBench使用
- 堆栈中的JS执行过程
- 减少判断层级
- 减少作用域链查找层级
- 减少数据读取次数
- 字面量与构造式
- 减少循环体中活动
- 减少声明及语句数
- 惰性函数与性能
- 采用事件绑定
JS性能优化
- 如何编写高性能的JS
- 随着软件开发的不断发展,性能优化是不可避免的
- 哪些内容可以看作是性能优化,本质上任何一种可以提高运行效率降低运行开销的行为都可以看作是一种优化操作
- 在软件开发过程中,必然存在很多值得优化的地方。尤其是在前端应用开发过程中,性能优化我们可以认为是无处不在的
- 例如:请求资源时用到的网络,数据的传输方式,开发过程中使用的框架
- 本阶段核心是JS语言的优化
- 从认知内存空间的使用
- 垃圾回收的方式介绍
- 性能优化概要
- 内存管理
- 为什么内存需要管理
- 内存管理的基本流程
- 垃圾回收与常见的 GC 算法
- 灵活地应对一些大场面试
- V8引擎的垃圾回收
- 使用的什么样的 GC 算法
- Performance工具
- 如何对内存进行监控
- 代码优化实例
- 内存管理
JS内存管理
- 随着硬件技术的不断发展,同时高级编程语言当中也都自带 GC 机制,这也变化让我们不需要特别注意内存空间使用情况下也能够完成正常的功能开发
- 内存为什么需要管理
function fn(){
arrList = []
arrList[100000] = 'bost'
}
- 如果我们在写代码时,不够了解内存机制,从而会让我们编写出一些不容易察觉到的内存问题性代码,这种问题多了以后给我们程序带来的可能就是一些意想不到的bug,所以掌握内存管理非常有必要
- 内存管理介绍
- 内存:由可读写单元组成,表示一片可操作空间
- 管理:由人主动去操作这片空间的申请,使用和释放
- 内存管理:开发者主动申请空间,使用空间,释放空间
- 管理流程:申请 — 使用 — 释放
- JS中的内存管理(ES当中,没有提供相应的操作API,所以JS不能像是C或者C++那样由开发者主动去调用相应的API来完成空间管理,但是这并不影响我们通过JS脚本来演示当前在内部一个空间的生命周期是怎样完成的)
- 申请使用空间
- 使用内存空间
- 释放内存空间
// 申请空间
let obj = {}
// 使用空间
obj.name = 'xl'
// 释放空间
obj = null
JS中的垃圾回收
-
JS中什么内容会被当作垃圾回收
- JS中内存管理是自动的
- 每当创建一个对象,数组,或者函数时,就会自动分配一块内存空间
- 对象不再被引用时是垃圾
- 后续程序在执行的过程中,如果通过一些引用关系,无法找到某些对象的时候,这些对象会被当作垃圾
- 对象不能从根上访问到时是垃圾
- 这些对象其实是存在的,但是由于代码当中一些不合适的语法或者结构性的错误,让我们没有办法再去找到这个对象,那这个对象也会被称为垃圾
- JS中内存管理是自动的
-
知道了什么是垃圾之后,JS执行引擎,就会出来工作,把他们占的对象空间进行回收,这个过程就是所谓的JS垃圾回收
JS中的可达对象
- 可以访问到的对象就是可达对象(引用 / 作用域链)
- 可达的标准就是从根出发是否能够被找到
- JS中的根可以理解为是全局变量对象
总结
- JS中的垃圾回收,其实就是找到垃圾然后让JS执行引擎来进行空间的一个释放和回收,在这里我们用到了引用和可达对象
JS中的引用与可达
- 引用
let obj = { name: 'xl'}
/**
* 这行代码相当于这个空间被obj引用了
*
* 当前这个obj是可以从根上找到的,所以这个obj是可达的
* */
let ali = obj
/**
* xl的空间又多了一次引用
*
* 所以存在了一次引用数值变化,这个概念之后引用计数算法当中会被用到
* */
obj = null
/**
* 这行代码之后
*
* 原本xl这个对象空间是由两个引用的,而随着这行的执行
*
* obj到xl空间的引用,相当于是断掉了
*
* 但现在当前这个xl对象还是可达的,因为ali还在引用着
*
* 这就是引用的主要说明,顺便看到了一个可达
* */
- 可达
function objGroup(obj1, obj2) {
obj1.next = obj2
obj2.prev = obj1
return {
o1: obj1,
o2: obj2
}
}
/**
* 首先,我们定义了一个函数并接受了两个变量,一个是obj1一个是obj2
*
* 然后在内部让其有一个属性互相指引
*
* 通过return返回这样一个结果,然后在外部调用函数
*
* 现在的对象不管是o1还是o2再或者是objGroup返回的这个对象
*
* 都可以从根上找到
* */
let obj = objGroup({name: 'obj1'}, {name: 'obj2'})
console.log(obj)
/**
* {
* o1: { name: 'obj1', next: { name: 'obj2', prev: [Circular] } },
* o2: { name: 'obj2', prev: { name: 'obj1', next: [Circular] } }
* }
* */
- 垃圾
- 我们现在对可达里边的代码进行一些操作
- 将obj对象内部对obj1的引用删除,然后将obj2.prev删除
- 这个时候我们没有办法通过任何方式获取到obj1
- 这时o1会被认为是一个垃圾,最后JS引擎会找到他并进行一个回收
- 我们当前在编写代码时,会存在一些对象引用的关系,我们可以从根的下边来进行查找,按照链条,我们终归能找到某一个对象,如果说找到对象的一些路径被破坏掉,或者被回收了,这个时候我们是没办法再找到他的,最后我们会把她当作垃圾,可以让垃圾回收机制将其回收掉
GC算法介绍
- GC定义
- GC可以理解为就是垃圾回收机制的简写
- GC可以帮找到内存中的垃圾,并释放和回收空间
- GC里边垃圾是什么
- 程序当中不再需要使用的对象 — 如果我们某一个数据,在使用完成之后上下文里边不再需要用到他了,我们就可以把它当作垃圾看待,例如name,当函数调用完成后,我们就不再需要使用name了,从需求的角度考虑,他应该是被当作垃圾然后被回收掉的,至于有没有被回收先不考虑
function func() { name = 'xl' return `my name is ${name}` } func()
- 程序中不能在访问到的对象 — 在函数调用结束之后,我们在外部的空间当中,就不能在访问到name,所以当我们找不到它的时候,其实它也可以算作是一种垃圾
function func() { const name = 'xl' return `my name is ${name}` } func()
- GC里边垃圾是什么
- 什么是GC算法
- GS是一种机制,垃圾回收器完成具体的回收工作
- 工作的内容就是查找垃圾 / 释放空间 / 回收空间
- 算法就是工作时查找和回收所遵循的规则
- 如何去查找空间
- 释放空间时应该如何释放
- 回收空间如何去进行分配
- 这些过程必然有不同的方式,这个GC的算法就可以理解为上述的垃圾回收器在工作过程中所遵循的一些规则
- 常见GC算法
- 引用计数
- 通过一个数字来判断当前这个变量时是不是一个垃圾
- 标记清除
- 在GC工作的时候给那些活动的对象添加上一个标记,然后判断是不是一个垃圾
- 标记整理
- 与标记清除很类似,我们后续回收的过程中可以做一些不同的事情
- 分代回收
- 在V8中会用到
- 引用计数
引用计数算法实现原理
- 核心思想:在内部,通过一个引用计数器来维护当前对象的引用数,从而判断当前引用数是否为0来判断它是否是一个垃圾对象。当这个数值为0时,GC就开始工作,将其所在的对象空间回收和释放在使用
- 引用计数器:相对于其他一些GC算法来说,也正是由于引用计数器的存在,导致了引用计数再执行效率上,可能有其他的算法有区别
- 当某一个对象的引用关系改变时,引用计数器主动修改当前对象所对应的引用数值
- 假设代码中现在有一个引用空间,目前来说有一个变量名指向它,这个时候我们就把数值加1,如果在说这个时候又多一个对象还是指向它,那么我们就把数值在加1,如果是减少的情况就减1,如果为0时,那么GC就会立即工作将当前的对象空间回收
const user1 = {age: 18}
const user2 = {age: 28}
const user3 = {age: 38}
const nameList = [user1.age, user2.age, user3.age]
function fn() {
const num1 = 1
const num2 = 2
}
fn() // 不可以获取内部变量
function fn1() {
num1 = 1
num2 = 2
}
fn1() // 可以获取到内部变量
- 总结:其实就是靠着当前对象身上的一个引用计数的数值,来判断是否为0,从而决定它是不是一个垃圾对象
引用计数算法优缺点
优点
- 发现垃圾时立即回收
- 它可以根据当当前这个引用数是否为0来决定这个对象是不是一个垃圾,如果找到了,那就可以立即进行释放
- 最大限度减少程序暂停
- 我们的应用程序再执行的过程中,必然会对内存进行一个消耗,而我们当前的执行平台他的内存肯定会有上限的,所以内存肯定会有沾满的时候,不过由于引用计数算法他是时刻监控着那些引用数值为0的对象,所以我们就可以认为,当他发现这个内存即将饱满时,那么引用计数就会立马去找到那些数值为0的计算空间,然后对其进行释放,这样就保证了我们当前的一个内存是不会有占满的时候
缺点
- 无法回收循环引用的对象
- 时间开销大
- 因为当前的引用计数它需要去维护一个数值的变化,所以在这个情况下,他要时刻的去监控着当对象的一个引用数值是否需要修改,本身来说,这个对象的数值修改就需要消耗时间,如果说这个内存里边有更多的对象需要修改,那么这个时间就会显得更大一些。
function fn() {
const obj1 = {}
const obj2 = {}
obj1.name = obj2
obj2.name = obj1
return 'xl'
}
fn()
/**
* 我们这个函数执行结束后,内部所在的空间,肯定会涉及到空间回收的情况
*
* 比如说obj1 obj2 因为在全局的地方我们已经不在直接去指向他了
*
* 所以这个是他的引用计数应该为0
*
* 但是现在会有一个问题:
*
* 在里边当我们想要去找GC来把obj1删除时,obj2现在有一个属性指向obj1
*
* 换句话说,按照之前的规则,我们在全局作用域下,找不到obj1和obj2了
*
* 但是由于他两者之间在同一作用域内明显还有互相指引的关系
*
* 所以在这种指引下他们身上的引用计数器的数值还不为0
*
* 这时引用计数算法下的GC就没有办法将其回收,从而造成了内存空间的浪费
*
* 这就是所谓的对象之间的循环引用
* */
标记清除算法实现原理
- 相对于之前的引用计数算法,这个算法的原理实现更加简单,而且还能拿个解决一些相应的问题,在后续的V8当中会被大量的使用到
- 核心思想:分标记和清除两个阶段完成
- 遍历所有对象,找到活动对象,进行标记
- 活动对象和可达对象是一个道理
- 遍历所有对象,没有标记的对象进行清除
- 注意:第二阶段会把第一阶段设置的标记抹除,便于GC下次还能正常工作
- 遍历所有对象,找到活动对象,进行标记
- 通过两次遍历行为回收相应的空间,最终再交给相应的空闲列表进行维护,后续我们的程序代码可以实现使用
标记清除算法优缺点
- 作为一个GC算法,它依然是做不到完美无瑕的
优点
- 解决对象循环引用的回收操作
缺点
- 回收完成之后地址不连续,后续我们想去申请一片空间,刚好申请的空间大小是1.5格子,这种情况下,有的回收空间是两个格子,有的是一个格子,这个时候造成了一个当前标记清除算法中最大的问题 — 空间的碎片化
- 空间碎片化 ---- 由于我们当前所回收的垃圾对象,在地址上他本身是不连续的,由于这种不连续从而造成了,我们在回收之后他们分散在各个角落,后续我们想去使用时,新的生成空间刚好与他们大小匹配,那就可以直接使用,一旦是多了或者少了,就不太适合使用了,所以这就是我们标记清除算法当中的一个缺点
标记整理算法实现原理
- 和标记清除算法一样,在V8当中经常会被使用到
- 标记整理其实可以看作为标记清除的增强操作
- 因为标记阶段的操作和标记清除是一样的,因为他们都是遍历所有对象,然后将当前的可达活动对象进行标记
- 清除阶段时,标记清除算法是直接将未被标记的垃圾对象做空间的回收,但是标记整理会在清除之前,先执行一个整理的操作,移动对象的位置,去让他们能够在地址上产生一个连续
常见GC算法总结
- 引用计数
- 在内部去通过一个引用计数器来维护每个对象都存在的一个引用数值,通过这个数值是否为0来判断这个对象是否为垃圾对象,从而去回收他的一个垃圾空间
- 优缺点
- 可以即使回收垃圾对象
- 减少程序卡顿时间
- 无法回收循环引用的对象
- 资源消耗较大
- 标记清除
- 标记 — 遍历所有对象,然后给当前活动对象标记
- 清除 — 遍历所有对象,然后给没有被标记到的对象的空间回收
- 优缺点
- 可以回收循环引用的对象
- 容易产生碎片化空间,浪费空间
- 不能够立即回收垃圾对象
- 标记整理
- 标记 — 遍历所有对象,然后给当前活动对象标记
- 清除 — 遍历所有对象,在清除之前,先执行一个整理的操作,移动对象的位置,去让他们能够在地址上产生一个连续,然后给没有被标记到的对象的空间回收
- 优缺点
- 减少碎片化空间
- 不会立即回收垃圾对象
认识V8
- V8是目前市面上最主流的JS执行引擎
- 目前我们的chrome浏览器,nodejs平台都在采用
- JS之所以能在上边高效的工作,正是因为V8的存在
- V8采用即时编译
- 之前的引擎都需要先将我们的JS代码转为字节码,然后才能去执行,对于V8来说,可以直接将源码直接翻译为我们当前可以直接执行的机器码,所以这个时候的速度是非常快的
- V8内存设有上限
- V8的内存空间设置了一个数值,在64位的操作系统上不能超过1.5G,对于32位来说这个数值不能超过800MB的
- 为什么
- V8本身就是为了浏览器来去制造的,所以现有的内存大小对于网页应用来说是足够的
- V8内部所去使用的一个垃圾回收机制决定了采用这样一个设置是合理的,因为官方做过这样一个测试,当我们的垃圾内存达到1.5G的时候,如果V8去采用增量标记的算法进行垃圾回收只需要消耗50ms,而如果采用非增量标记的话则需要1s,从用户体验来说,1s其实已经算是很长的时间了,所以V8就是用1.5G为界限了
V8垃圾回收策略
- 前置描述:我们都知道,在程序的使用过程中,我们会用到很多数据,而这些数据我们可以分为原始数据和对象类型的数据,对于这些基础的原始数据来说,都是由程序的语言自身来进行控制的,所以在这里我们所提到的回收,主要还是指的是当前存活在我们堆区里的对象数据,因此这个过程我们是离不开对象操作的,而我们当前也知道,在V8当中,他对内存是做了上限的,所以我们接下来主要看一下他是怎样对垃圾进行回收的呢
- 采用分代回收的思想 — 把我们当前的内存空间区按照一定的规则分成两类,一个是新生代存储区,一个是老生代存储区,至于如何划分我们后续会介绍到,有了这样的一个划分之后,它就会根据不同代采用更高效的一种GC算法,从而对不同对象进行一个回收的操作
- V8常用GC算法
- 分代回收 — 必须用到,因为要去做空间分代
- 空间复制 — 后续会介绍
- 标记清除
- 标记整理
- 标记增量 — 为了提高效率
V8如何回收新生代对象
概述
- V8内存空间一分为二
- 小空间用于存储新生代对象(32MB || 16MB)
- 新生代指的是存活时间较短的对象
- 局部作用域内的变量就属于新生代
回收流程
- 回收过程采用复制算法 + 标记整理
- 首先将左侧的小空间也一分为二,大小相同
- 使用空间为From,空闲空间为To
- 将所有的变量对象都分配在From空间
- 现在To是空闲状态,一旦我们的From空间应用到一定程度,去触发GC操作,这时会采用标记整理的操作将活动对象拷贝至To
- 拷贝完成就意味着我们之前的From空间的对象有了一个备份,这时就可以考虑做一些回收操作了
- 将From空间进行完全的释放,因为From里的对象在To里边有一个备份,From直接释放掉就不存在任何问题
- 这时新生代对象就完成了一个回收操作
回收细节说明
- 拷贝过程中可能出现晋升
- 如果我们在拷贝时发现某一个变量对象所指定的空间呢,他在我们当前的老生代对象里边也会出现,这时我们就会出现一个所谓的 晋升 的操作
- 晋升就是将新生代对象移动至老生代
- 一轮GC还存活的新生代需要晋升
- 当前在拷贝的一个过程中发现To空间的使用率超过了25%,那么在这时我们也需要将这一次的活动对象移动至老生代
- 为什么是25%?
- 因为我们在将来进行回收操作的时候需要把Form空间和To空间进行一个交换,也就是说以前的To会变成From,以前的From会变为To,这也就意味着我们的To使用率如果达到了80%,那么最终他变成活动对象的存储空间后,新的对象就存不进来了
- 为什么是25%?
V8如何回收老生代对象
概述
- 老生代对象存放在右侧老生代区域
- 针对于老生代区域,在V8当中同样有一个内存大小的限制
- 64位操作系统1.4G,32位操作系统700MB
- 老生代对象其实指的就是当前存活时间较长的对象
- 例如:全局对象下的变量,闭包内的变量数据
回收流程
- 主要采用标记清除,标记整理,增量标记算法
- 首先使用标记清除完成垃圾空间的回收
- 存在空间碎片化的问题
- 如果我们发现当他去想把我们新生代区域的内容,往老生代进行移动的时候,而且这个时间节点上我们老生代存储区域的共建又不足以来存放新生代存储区所以移动过来的对象,也就是我们之前提到过的晋升,这种情况就会触发标记整理,将之前的碎片化空间整理为地址上是连贯的
- 采用增量标记进行效率优化
回收细节对比
- 新生代区域垃圾回收使用空间换时间
- 采用的是复制算法,意味着每时每刻内部都会有一个空闲空间的存在,但是由于新生代存储区,本身的空间就很小,所以分出来的空间就更小,所以这部分空间的浪费,相对于带来的时间上的提升是微不足道的
- 老生代区域垃圾回收不适合复制算法
- 老生代空间很大,一分为二的话就有几百MB的空间是浪费不用的,这样就太过于浪费
- 老生代空间存储的数据其实比较多,所以在复制时消耗的时间也会更多
V8垃圾回收总结
- V8是一款主流的JS执行引擎
- V8内存设置上限
- 本身是针对与浏览器设置的,所以在webAPP上,这个内存是充足的
- 内部的垃圾回收机制来决定,如果内存再放大,那垃圾回收时间就可能会超过用户的感知,影响体验
- V8采用分代回收思想实现垃圾回收
- 新生代
- 老生代
- V8垃圾回收常见的GC算法
- 新生代
- 复制算法 + 标记整理算法
- 老生代
- 标记清除算法 + 标记整理算法 + 增量标记算法
- 新生代
Performance工具介绍
- 关于GC的目的就是为了实现内存空间的良性循环
- 良性循环的基础是对内存空间有一个合理的使用
- 时刻关注才能确定是否合理
- Preformance提供多种监控方式
总结:
- 通过Performance时刻监控内存,有这样一个操作后,我们就可以在程序的内存出现一些问题之后,直接想办法定位到当前问题所在的代码块
基本使用步骤
- 打开浏览器输入目标网址
- 进入开发人员工具面板,选择性能
- 开启录制功能,访问具体界面
- 执行用户行为,一段时间后定制录制
- 分析界面中记录的内存信息
内存问题的体现
- 页面出现延迟加载或者经常性暂停 — 频繁的垃圾回收
- 页面持续性出现糟糕的性能 — 内存膨胀
- 页面的性能随时间延长越来越差 — 内存泄漏
监控内存
界定内存问题的标准
- 内存泄漏 — 内存使用持续升高
- 内存膨胀 — 在多数设备上都存在性能问题
- 频繁的垃圾回收 — 通过内存变化图进行分析
监控内存的几种方式
- 浏览器任务管理器
- 可以直接以数值的形式,将我们当前应用程序在执行过程中内存的变化体现出来
- Timeline时序图记录
- 直接把我们应用程序执行过程中所有内存的走势以时间点的方式呈现出来,有了这张图很容易做判断
- 堆快照查找分离DOM
- 有针对性的查找我们当前的界面对象中,是否存在分离的DOM,因为分离DOM的存在就是一种内存泄漏
任务管理器监控内存
- 如果我们最后一列小括号内的数值一直增大,那就意味着这个内存是有问题的
- 具体来说是什么问题当前这个工具就显得不是特别好用了,因为它只能帮助我们发现这个地方有没有问题,如果说我们想定位问题时,他就不太好用了
- 在这个地方我们可以直接通过
shift + esc
调出任务管理器 - 找到我们想要去监控的具体脚本,也就是说web页面
- 选中之后如果说没有JS这一列我们可以直接右键然后勾选
- 调整完后我们只需要关注两列
- 第一列为当前DOM节点占用的内存,一般情况也是不变为好,如果要变的话就证明我们当前界面存在频繁的DOM操作
- 第二列为最后的JS内存,在这里我们要关注的就是小括号内的数值,得出的结论就是如果小括号里的数值一直增加而没有变小的过程,就意味着我们的内存就一直往上走,而没有GC消耗,所以这个时候就有问题了
Timeline记录内存
- 任务管理器可以帮助我们发现问题,但是具体定位的话就显得不是很方便
- Timeline — 通过时间线记录内存变化的方式 — 更精确的定位到我们当前内存的问题与那一块代码是相关的,或者说在什么时间节点上发生的
堆快照查找分离DOM
- 什么是分离DOM
- 界面元素存活在DOM树上
- 垃圾对象时的DOM节点
- 分离状态的DOM节点
- 总结:
- 我们可以利用浏览器当中提供的堆快照的功能,然后把我们当前的堆进行性拍照,拍照结束后找这里边是否存在一些分离DOM,因为这个分离DOM在界面中不体现,但是在内存中的确存在,所以这个时候它是一种内存的浪费,我们要做的就是定位到我们的代码里的那些分离DOM所在的位置,然后想办法给清除掉
判断是否存在频繁GC
- 为什么需要知道是否存在频繁的垃圾回收
- 当GC工作时应用程序是停止的
- GC频繁工作而且时间过长,对web应用很不友好
- 因为它会处于一个假死的状态,对于用户来说就会感觉到整个应用有点卡顿
- 所以就要想办法确定当前应用中是否存在着频繁的回收
- Timeline中频繁的上升下降
- 任务管理器中数据频繁的增加减小
Performance总结
- 谷歌浏览器提供的一个性能工具
- Performance使用流程
- 内存问题相关分析
- 内存泄漏
- 内存膨胀
- 频繁的垃圾回收
- Performance 时序图监控内存变化
- 任务管理器监控内存变化
- 堆快照查找分离DOM
代码优化介绍
如何精准测试JS性能
- 本质上就是采集大量的执行样本进行数学统计和分析,从而得到一个比对的结果,什么样的脚本执行效率更高,这样一个过程对于我们编码者来说显得有些麻烦,因为我们可能更多地只是关注该如何使用脚本实现某一个功能,而不是去做大量的数学统计
- 所以在这里我们采用 基于
Benchmark.js
的https://jsperf.com/
完成
Jsperf 使用流程
- 使用GitHub账号登录
- 填写个人信息(非必填)
- 填写详细的测试用例信息(title、slug)
- sulg — 必须是唯一的,因为他要生成一个空间来利于我们去访问自己的测试用例
- 填写准备代码(DOM操作时经常使用)
- 填写必要有setup与teardown代码
- 填写测试代码片段
慎用全局变量
- 在程序执行过程中,如果针对于某些数据需要进行存储,那我们可以尽可能的把它放置在局部作用域中变成一个局部变量
- 当我们在一个全局作用域内定义一个变量后,他其实就是存在于全局的执行上下文当中,是所有作用域链的顶端
- 如果按照这种层级向上查找的一个过程来说,那下边某些局部作用域没有找到的变量,最终都会去查找到最顶端的全局执行上下文,所以这种情况下,我们的时间消耗是非常大的,这样一来就降低了我们当前代码的执行效率
- 在当前全局上下文当中去定义的变量,他一直是存活于上下文执行栈,这个栈是直到我们当前程序退出之后才会消失的
- 这对我们当前的GC工作来说也是非常不利的,因为只要我们的GC发现这样一个变量,属于一个存活的状态我们就不会把他当做垃圾对象进行回收,因此这样的做法也会降低我们当前的程序运行过程中对于内存的一个使用
- 如果某个局部作用域当中出现了同名全局变量则有可能会遮蔽或污染全局
- 总体来说,我们使用全局变量时需要考虑更多的事情,否则就会给我们带来一些意想不到的情况
缓存全局变量
- 在我们程序的执行过程中,有些地方,我们针对于这样的全局变量使用,是无法避免的,例如:查找DOM时,我们必须要使用document,而这个document并不是由我们自己直接定义的,而是存在于当前顶层对象下边,内置好了,可以直接用
- 这时我们可以选择把大量需要重复使用的一个全局变量放置到一个局部作用域当中,从而达成一种缓存的效果
通过原型对象添加附加方法
- 在JS当中存在三种概念
- 构造函数
- 原型对象
- 实例对象
- 在这个过程当中,我们的实例对象和构造函数都是可以指向原型对象的,所以这个过程中如果某一个构造函数的内部局有一个成员方法,让我们后续的实例对象都需要频繁的去进行调用,那么在这里可以直接把他添加在原型对象上,而不需要把他放在我们的构造函数内部,而这样两种实现方式呢,在进程上也会有所差异。
避开闭包陷阱
- 外部具有指向内部的引用
- 在 外 部作用域内访问 内 部作用域的数据
function foo () {
var name = 'xl'
function fn () {
console.log(name)
}
return fn
}
var a = foo()
a()
- 关于闭包
- 闭包是一种强大的语法
- 闭包使用不当很容易出现内存泄露
- 不要为了闭包而闭包
// function foo() {
// var el = document.getElementById('btn')
// el.onclick = function () {
// console.log(el.id)
// }
// }
// foo()
/**
* 标准闭包
* 1. 外部对内部有引用
* 2. 突破作用域的限制,在一个作用域中用到另一个作用于的数据
*
* 因为它是闭包,所以会产生一个不好的影响,就是当前这样一个内存是会泄露的
*
* 由于闭包语法的存在,导致我们函数中的一些变量是没有办法被回收
*
* 这种情况下类似于这样的操作越来越多的时候,对我们的内存来说是非常不友好的
*
* 当我们的内存一直被消耗,我们程序的性能肯定是越来越差
* */
function foo() {
var el = document.getElementById('btn')
el.onclick = function () {
console.log(el)
}
el = null
}
foo()
/**
* 关于btn这个元素其实本身就已经存在于我们的DOM当中或者说在body里
*
* 所以就算我们这里边不通过el再去引用这样的一个必填的DOM节点,那他本身也会有一个引用的存在
*
* 而我们在这个地方是相当于在它的引用之上,又去让一个el也引用了这样一个DOM节点
*
* 所以我们也可以理解为是这样一个DOM节点呗我们引用了两次,如果说在将来某一个时间点下
*
* 我们这个DOM节点从我们的界面中消失了,也就是说删除或者清除的行为
*
* 这是我们要想明白,我们在界面把元素删除,也就意味着在DOM上的引用就不存在了
*
* 但是会有一个问题:代码里的el对我们之前的DOM还是有引用的,所以根据我们之前所提到的引用计数这个原则
*
* 我们这个DOM对象其实只是减少了一个引用,而代码里边仍在引用这他,所以我们的垃圾回收机制在工作时无法回收
*
* 因此我们的泄露依然存在,而如果说加了 el = null 时,当我们在界面当中去把页面的元素清除掉之后,DOM对它的引用就消失了
*
* 而代码里边对他的引用也消失了,这个时候我们这个样一个内存空间得以释放,这也是这行代码存在的意义
* */
- 这就是如何避开闭包的陷阱,而提高整个程序的性能,不是我们两段代码在执行速度上的一个对比,指的是整个内存层面上的性能对比
避免属性访问方法使用
- 关于属性访问语法,这是和面向对象相关的,为了更好地实现这样一个封装性,所以在更多的时候我们可能会讲一些对象的基本属性和方法,放在一个函数的内部,然后在外部暴露一个方法对当前这个属性进行增删改查,但是这个特性在JS特效中其实并不是那么的适用,因为在JS中不需要属性的访问方法,所有的属性在外部都是可见的
- 而且在使用属性访问方法的时候,他就相当于是增加了一层重定义,对当前的访问控制是没有太多的一个意义,所以不去推荐
function Person1() {
this.name = 'xl'
this.age = 18
this.getAge = function () {
return this. age
}
}
const p1 = new Person1()
const a = p1.getAge() // 属性访问方法
function Person2() {
this.name = 'xl'
this.age = 18
}
const p2 = new Person2()
const b = p2.age // 直接访问
For循环优化
- for循环是我们经常会去使用的语法结构,每当我们遇到一个数组结构或者类数组时,我们想要把其中一些值进行拿出使用时,都可以使用for循环对其进行遍历
for (var i = 0; i < btns.length; i++) {
console.log(i)
}
for (var i = 0, len = btns.length; i < len; i++) {
console.log(i)
}
选择最优的循环方法
- for、foeEach、for…in…
var arrList = new Array(1, 2, 3, 4, 5, 6, 7, 8, 9)
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(i)
}
// forEach > for > for..in..
文档碎片优化节点添加
- 针对于web应用的开发来说,DOM节点的操作是很频繁的,而针对于DOM的交互操作又很消耗性能,特别是创建一个新的节点,将它添加至界面中时,这个过程会伴随着回流和重绘的出现
- 回流和重绘对于性能的消耗又很大
for (var i = 0; i < 10; i++) {
var op = document.createElement('p')
op.innerHTML = i
document.body.appendChild(op)
}
const fragEle = document.createDocumentFragment()
for (var i = 0; i < 10; i++) {
var op = document.createElement('p')
op.innerHTML = i
fragEle.appendChild(op)
}
document.body.appendChild(fragEle)
克隆优化节点操作
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 newP = oldP.cloneNode(false)
op.innerHTML = i
document.body.appendChild(op)
}
直接量替换new Object
var a = [1, 2, 3]
var a1 = new Array(3)
a1[0] = 1
a1[1] = 2
a1[2] = 3
// a > a1
JSBench使用
- 一个可以在线测试JS代码执行效率的网站
- JSBench.me
- 为什么要使用JSBench
- 因为我们一直使用jsperf这个网站已经不再维护了,所以没有办法使用了
- 我们有一些JS相关的内容需要去分享,而通过整个网站可以很方便的去在我们去测试部分代码时直接去进行使用
- 我们需要说明的是,其实测试我们这种代码的执行效率的网站其实有很多,比如我们可以直接使用代码进行时间统计,还有其他的在线网站
- 选择这个网站也是综合考虑了他的打开速度以及在使用的快捷方面
- 注意点:
- 当前这个工具在做测试是尽量只保留一个标签页
- 在执行代码测试时,尽可能让这个页面停留在这里,或者说当前页面不要关掉
- 不能执行一遍得出的结论和我们想要的结果一样或者不一样时就认为它是合理的或者是最终的答案,我们应该让当前脚本多执行几次,然后取几率更高的结果
- 性能测试过程中,我们不应该去纠结于这个代码的执行时间,我们当前去使用这个工具去运行的代码其实主要的还是跑一下代码执行的速度,但是对于我们的性能测试来说,我们关注的他并不是只有时间,他只是众多性能指标当中的一个,一段代码的执行速度快并不意味着他就很健壮
- 对于我们的代码在执行的过程中所涉及到的性能,其实就是两个方面,要么拿空间换时间,要么拿时间换空间
堆栈中的JS执行过程
- 这段我们来看一下关于代码在执行过程中偏底层发生的一些事情,这样的操作可以更加具象的表达出一段代码在栈内存和堆内存里是如何执行的,同样的也有利于我们理解GC回收内存的工作过程
let a = 10;
function foo (b) {
let a = 2
function baz (c) {
console.log(a + b + c)
}
return baz
}
let fn = foo(2)
fn(3)
减少判断层级
- 具体来说就是在我们编写代码的过程中,有可能会去出现判断条件嵌套的场景,往往再出现if…else多层嵌套的时候,我们都可以去通过提前return掉无效的条件来达到嵌套层级的优化效果
// function dys (part, chapter) {
// const parts = ['ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020']
// if (part) {
// if (parts.includes(part)) {
// console.log('当前属于这个模块')
// if (chapter > 5) {
// console.log('您需要提供VIP')
// }
// }
// } else {
// console.log("请确认模块信息")
// }
// }
// dys('ES2016', 6)
function dys (part, chapter) {
const parts = ['ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020']
if (!part) {
console.log("请确认模块信息")
return
}
if (!parts.includes(part)) return
console.log("当前属于这个模块")
if (chapter > 5) {
console.log("您需要提供VIP")
}
}
dys('ES2016', 6)
/**
* 优化的核心思想和作用域链的查找和内存的空间的使用并不是那么的有关系或者直观
*
* 因为我们是从代码的整体的一个写法进行的改变
*
* 首先就是代码量
*
* 再有就是上边的代码就是一层一层的套,下边的直接去判断一层够了
*
* 相当于我们判断问题的算法得到一个改变
*
* 明确类条件的分支建议使用 switch..case...
*
* if..else...一般适合于区间的条件判断,如果我们明确了几个枚举值的时候建议使用switch...case...
*
* 这样代码会更加清晰,易于维护
*
* 不过易于维护的代码并不代表执行速度就很快,这点对于性能优化来说还是要看我们需要什么来决定的
* */
减少作用域链查找层级
- 每当一个函数执行的时候会产生一个执行上下文,例如我们当前代码中的foo函数,他的上下文就定义了函数执行的环境,当这个该函数执行结束这个上下文就会被销毁掉,这也取决于是否有闭包的存在,因此我们多次调用函数时就会多次创建上下文,这些上下文他都是有自己的作用域的,这些作用域之间呢又可以去通过作用域链进行连接,所以在函数执行过程中,就会先去搜索他自己的作用域,例如fn函数中需要访问age和name,他首先会在自己内部找,发现有age那就用自己的就可以,没有name,这个时候会沿着作用域链向上查找,他上层就是父作用域,如果在父作用域内没有找到,就继续往上找,父的上层在我们的代码中其实就是全局,这样一层一层的查找,基于这样的查找过程,如果说变量存在于离我们当前fn更近的地方,那查找所消耗的时间就会更快一些,那代码的执行效率会更高
// var name = 'xl'
// function foo () {
// name = 'xl666' // name属于全局 --- 修改了全局变量
// function fn () {
// var age = 38
// console.log(age)
// console.log(name)
// }
// fn()
// }
// foo()
var name = 'xl'
function foo () {
var name = 'xl666' // name属于局局 --- 修改了局部变量
function fn () {
var age = 38
console.log(age)
console.log(name)
}
fn()
}
foo()
减少数据读取次数
- 这种的快,是建立在我们空间的消耗上的提速,后续主要看我们的需求是看重速度还是空间
// var box = document.getElementById('skip')
// function hasbox (ele, cls) {
// return ele.className === cls
// }
// console.log(hasbox(box, 'skip'))
var box = document.getElementById('skip')
function hasbox (ele, cls) {
var clsname = ele.className
return clsname === cls
}
console.log(hasbox(box, 'skip'))
字面量与构造式
- 不同的数据声明方式和性能的关系,我们肯定都听过在编码是使用字面量来代替构造的方式创建数据,但是这种方式是有一些特定场景的,例如我们在处理日期,数组类型的数据的时候,无论我们是采用字面量还是构造的方式,我们最终得到的数据他们都是引用类型的
// let test = () => {
// let obj = new Object()
// obj.name = 'xl'
// obj.age = 18
// obj.slogan = '你好小鹿'
// return obj
// }
// console.log(test())
/**
* 上边的方式就好像是在调用一个函数,而下边在创建时其实就是直接去开辟空间在里边存东西
*
* 这个过程又涉及到了一个函数的调用,所以就相当于他做的事情更多一些
* */
let test = () => {
let obj = {
name : 'xl',
age : 18,
slogan : '你好小鹿'
}
return obj
}
console.log(test())
var str1 = 'xl说我为XX学代码'
/**
* 上边的代码其实就是一行字符串,下边的是一个对象
* */
var str2 = new String('xl说我为XX学代码')
减少循环体中活动
- 主要讨论循环的这样一个功能,而不是现在要去采用那种结构实现一种循环,所以这里就只是采用了for结构
- 放在循环体中的,往往都是一些我们想要去重复执行的事情,再循环次数固定的情况下,循环体做的事情越多,那执行效率就会越慢,反之效率就会越高
- 所以第一个思路就是把每次循环都要去用到的或者说都要操作的数据值不变的都抽离到外部去完成,这个道理类似于数据缓存
// var test = () => {
// var arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// for (var i = 0; i < arr.length; i++) {
// console.log(arr[i])
// }
// }
// test()
// var test = () => {
// var arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
// i ,
// len = arr.length
// for (i = 0; i < len; i++) {
// console.log(arr[i])
// }
// }
// test()
var test = () => {
var arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
len = arr.length
while (len--) {
console.log(arr[len])
}
}
test()
减少声明及语句数
- 代码编写中的小细节与性能的关系
- 变量的声明
- 语句数的多少,其实就是表达式的多少
- 我们都知道,程序员是可以记录、保存以及修改状态的,而正是这些状态让我们可以利用这些语言完成很多复杂的功能,很明显,如果我们要去记录这些功能,我们就应该消耗更多的空间,因此对于那些我们后续不需要轮番使用的数据,我们应该去使用的时候直接进行获取,而不是提前去做缓存
- 目的在于可以降低程序在执行时当前对内存的消耗
var box = document.getElementById('box')
var test = (ele) => {
let w = ele.offsetWidth
let h = ele.offsetHeight
return w * h
}
console.log(test(box))
var box = document.getElementById('box')
var test = (ele) => {
return ele.offsetWidth * ele.offsetHeight
}
console.log(test(box))
var test = () => {
var name = 'xl'
var age = 18
var slogan = 'xl666lx'
return name + ' ' + age + ' ' + slogan
}
console.log(test())
var test = () => {
var name = 'xl',
age = 18,
slogan = 'xl666lx'
return name + ' ' + age + ' ' + slogan
}
console.log(test())
惰性函数与性能
- 我们在使用的时候往往会把惰性函数看成高阶函数
var box = document.getElementById('box')
function foo () {
console.log(this)
}
// function addEvent (obj, type, fn) {
// if (obj.addEventListener) {
// obj.addEventListener(type, fn, false)
// }else if (obj.attachEvent) {
// obj.attachEvent('on' + type, fn)
// } else {
// obj['on' + type] = fn
// }
// }
// addEvent(box, 'click', foo)
function addEvent (obj, type, fn) {
if (obj.addEventListener) {
addEvent = obj.addEventListener(type, fn, false)
}else if (obj.attachEvent) {
addEvent = obj.attachEvent('on' + type, fn)
} else {
addEvent = obj['on' + type] = fn
}
return addEvent
}
addEvent(box, 'click', foo)
采用事件绑定
- 事件委托 — 利用事件冒泡把原本需要绑定在子元素上的响应时间委托给了父元素,让父元素去完成时间的监听,好处是减少了大量内存的占用,从而也可以去减少事件的注册,针对他的考点呢,我们一般给去一堆li的列表结构,要求不用考虑兼容性的问题,在这种情况下给每一个li添加一个事件处理,然后最终要达到的一个效果就是性能最优,这个时候可以直接采用事件委托
// var list = document.querySelectorAll('li')
// function showTxt(ev) {
// console.log(ev.target.innerHTML)
// }
// for (let item of list) {
// item.onclick = showTxt
// }
var ul = document.getElementById('ul')
function showTxt(ev) {
var obj = ev.target
if (obj.nodeName.toLowerCase() === 'li') {
console.log(obj.innerHTML)
}
}
ul.addEventListener('click', showTxt, true)