此篇JS性能优化主要是针对写代码过程中我们容易产生的一些容易忽视的小问题
内存管理
因为没有专门的函数调用方法,所以对我们来说,对于内存管理分为3个阶段段,主要是申请内存——使用内存——释放内存
垃圾回收与GC算法
- JS是如何产生垃圾的?
● JS中的内存管理是自动的
● 当对象不再被引用的时候会视为垃圾
● 对象不能从根本访问到的时候 - GC算法做了什么?
对垃圾的查找——释放空间——回收空间 - 都有哪些GC算法? 引用计数、标记清除、标记整理、分代回收
引用计数
- 设置引用数,判断当前引用数是否为0,当引用关系发生变化的时候更改引用数
- 主要看变量所处的环境来判断引用数值的大小
- 优点:发现垃圾时可以立即回收、最大限度减少了程序的暂停
- 缺点:无法函数内部两个对象的互相引用做处理,时间开销大
标记清除
- 分为标记阶段和清除两个阶段来完成的
- 首先是所有对象找标记活动对象,然后遍历清除没有标记对象
- 回收响应的空间,加入到空闲回收列表中进行后续空间安排
- 优点:可以解决引用计数对于函数内部互相引用的问题
- 缺点:标记清除所回收的空间是碎片化的,不能够合适地进行连续空间分配
标记整理
区别标记清除在于,清除之前会对活对象空间和回收空间进行整理归类,这样回收的空间是连续的,并且可以大量进行空间分配
V8引擎的垃圾回收
认识V8
- 主流的Javascript的执行引擎,采用即时编译是可以直接将源代码翻译成机器码执行,并且对于内存设限,是因为自身具有回收机制(64位1.5G内存满足浏览器)
- 能够对内存进行分类(新生代、老生代模式),不同模式下再采用不同的算法进行回收处理
新生代回收
- 主要占存大小为15GM,并且会平分为From、To两个等大空间
- 当申请内存时,会分配到From中,当达到存储后会触发算法回收
- 对活对象进行拷贝复制到To,然后清楚掉From就可以保留活对象,删除垃圾对象
- 经历了一轮GC算法还存活的新生代晋升为老生代,并且To空间使用率超过25%也需要晋升
老生代回收
- 主要采用的是标记清除、标记整理和增量标记
- 首先使用标记清除来做处理,当新生代进入老生代存在空间不足需要优化的时候,就采用标记整理出统一可用空间
- 增量标记是用来进行效率优化(增量标记,是解决不去阻塞程序执行的状况,在程序执行一段时间后,进行部分对象标记,然后再去执行程序,这样对于时间开销损耗是很合理的)
Performance工具
内存一般出现的问题?
- 内存泄漏:内存使用持续升高
- 内存膨胀:在多设备上都存在的性能问题
- 频繁垃圾回收:内存变化图来进行查看
监控内存的方法?
- 浏览器的任务管理工具(只能作为判断是否有问题)
- Timeline时序图记录(可以进行精准定位内存)
- 堆快照查找分离DOM(并没在页面展示,但占用内存的叫分离状态的DOM)
- 是否存在频繁的垃圾回收(DC太过于频繁会造成应用程序的假死)
V8引擎工作流程
预解析?scanner(扫描器,词法分析)、parse(解析器,构建语法树)—AST语法树?
- 能够跳过未被使用的代码,依据规范抛出错误,解析速度会更快
- 不生成AST,创建无变量引用和声明的scopes
- 声明未被调用时被认为不被执行的代码,进行预解析
全量解析?
- 解析被使用的代码生成AST
- 构建具体的scopes信息,变量引用和声明等,抛出所有语法错误
- 立即执行函数会被执行,所以只进行一次的全量解析
Ignition解释器
TurboFan编译器模块(字节码到机器码)
V8堆栈处理
代码执行步骤?
- 首先会开辟一个ECStack执行环境栈
- 形成一个全局执行上下文EC(G)管理不同代码
- VOG全局作用变化量
- 还有GO已经形成的window全局对象
- 对于每一个对象变量值都会开辟一个堆地址空间
函数堆栈处理
遇到代码做的一系列处理
- 创建函数和创建变量类似,函数名此时就可以看做是一个变量名
- 需要单独开辟一个堆内存来存放函数体(字符串形式代码),也是一个16进制地址
- 创建函数的时候,它作用域[[scope]]就已经确定了(创建函数时所在的执行上下文)
- 创建函数之后会将它地址存放在栈区关联起来
函数执行过程
- 函数执行的目的就是为了让函数对应的堆内存中的字符串形式代码进行执行,代码在执行时候肯定有一个环境EO(G),就意味着函数在执行的时候会生成一个新的执行上下文AO来管理函数体内的代码
- obj=[‘123’,‘456’’] —>[0:‘123’,1:‘456’]以键值对存储
- 在执行这个函数的过程中需要考虑到以下几方面
a. 确定作用域链:<当前执行上下,上级执行上下文>
b. 确定this的指向问题
c. 初始化arguments对象
d. 形参赋值 obj = arr
e. 变量提升
f. 执行代码
闭包中的堆栈处理
什么是闭包函数?
- 闭包是一种机制,是通过私有上下文来保护其中变量的机制
- 延长一个函数内部变量的生命周期,外部引用了一个函数内部的变量,当这个函数执行结束后,并不会被移除,类同于我们创建的某一个执行上下文不被释放的时候就形成了闭包
- 可以来保存、保护数据
是如何执行的?
用一个函数的具体执行过程来观察
-
对于每一步的执行操作,和之前所说的是一样的,其中包括变量提升,对函数开辟新的空间堆栈
-
遇到需要处理的函数,我们会将其当做函数变量来处理,变量值对应的是函数内部字符串存放的地址
-
对于var f = foo() 函数的调用执行过程,就是我们需要单独开一个函数执行上下文AC来做处理,执行完return这个匿名函数,全局的f中存放的值其实就是这个匿名函数字符串存放的地址0x001
-
通过发现可以看到EC(foo1)执行上下文中引用的堆0x001被EC(G)中的变量f引用着,所以foo()调用时所创建的执行上下文是不能被释放掉的
-
在执行f(5)、f(10)的过程中,就是传入形参进行赋值,创建单独的AC执行上下文环境,执行过程中会顺着函数所在的作用域链依次向上查找变量值执行,执行结束后,这个局部的AC执行上下文会被移除
总的来说,之所以作用域链上的Foo1不会被移除堆栈的原因是,外部f进行了对Foo1函数的引用,产生了闭包,所以不会被移除,而执行f(5)的时候发生的就是正常执行结束移除操作
闭包垃圾回收
foo(6)(7)
是作为一个立即执行函数来处理的,首先调用的是foo(6)返回的匿名函数是不会暂存,作为临时暂不被释放掉,然后直接传入7的参数去执行函数,只有当foo(6)(7)都被执行结束后才会被释放掉,但是其他闭包的被引用方式是会一直存在的
闭包引用的变量
函数最后执行结束,我们会发现只有全局的EC(G)和发生闭包引用的变量是没办法自动清除,占用内存空间的,那么我们就需要单独做处理才可以释放掉
- 浏览器自己本身的垃圾回收机制(内存管理)
- 对于堆、栈空间不同的处理
- 堆:如果当前堆内存被占用,就不能被释放掉,我们在确定后续不再使用这个内存数据的时候,可以自己主动置为空null,这样浏览器就会对其进行回收
- 栈:当前上下文中是否有内容被其他上下文变量所占用,如果有则无法释放
循环添加事件
原生代码实现事件添加
for(var i =0;i<aButton.length;i++){
aButton[i].onclick = function(){
console.log(`索引下表为${i}`)
}
}
如果我们想实现三个按钮点击输出对应的下标值,那么我们可以使用闭包和事件委托来实现
//闭包实现的三种可行方案
//利用立即执行函数
for(var i =0;i<aButton.length;i++){
(function(i)){
aButton[i].onclick = function(){
console.log(`索引下表为${i}`)
}
}(i)
}
for(var i =0;i<aButton.length;i++){
aButton[i].onclick = (function(i){
return function(){
console.log(`索引下表为${i}`)
}
})(i)
}
for(let i =0;i<aButton.length;i++){
aButton[i].onclick = function(){
console.log(`索引下表为${i}`)
}
}
//利用index来实现
for(var i =0;i<aButton.length;i++){
aButton[i].myIndex = i
aButton[i].onclick = function(){
console.log(`索引下表为${i}`)
}
}
//通过事件委托的方法实现
document.body.onclick = function(ev){
var target = ev.target
if(target.tagName === 'BUTTON'){
var index = target.getAttribute('index')
console.log(`索引下表为${index}`)
}
}
解析底部实现原理
原生通过var来添加事件机制实现
通过闭包来实现事件的添加机制
所以每次当我们通过闭包实现过程后,需要对变量以及DOM赋null值来实现垃圾回收
变量局部化
通过数据的存储和读取,来减少数据访问时需要查找的路径来提高代码的执行效率
全局、局部对比
在全局中每次去执行for循环的时候会发现,每次都是现在自己的作用域链上查找,如果没有找到就去找父级作用链,这样的话会多次查自己再向上查找
如果我们将变量设置为局部环境下的变量,就可以减少访问层级,不会存在每次都需要去找父级作用域链