小菜鸡的成长之路(函数性能优化)

486 阅读10分钟

写在前面

一直计划准备一些博客,种种原因搁浅(主要还是没时间。。。)。最近公司里的项目逐渐稳定下来了,前端的开发节奏虽然很快,但是都能实现,剩下的开发大多其实是繁复的代码组装的体力活。哈哈哈...遂给自己开了这个专栏,一是为了督促自己记录下编写过程和一些踩的坑,跟大家分享;二是写点自己想写的代码,让自己开心一些吧!

JAVASCRIPT性能优化

1、为什么要做性能优化

Javascript是一门非常灵活的语言,我们可以随心所欲的书写各种风格的代码,不同风格的代码也必然也会导致执行效率的差异,作用域链、闭包、原型继承、eval等特性,在提供各种神奇功能的同时也带来了各种效率问题,用之不慎就会导致执行效率低下。因此js的性能优化也逐渐被大家重视。

2、内存管理

2.1、内存

内存就是由可读写单元组成的可操作空间

2.2、管理

人为的操作内存空间

2.3、内存管理

  • 申请——开辟内存空间(var i)

  • 使用——赋值修改和删除 (i=18)

  • 回收——当开辟的内存空间不被任何变量指向的适合即为垃圾,将被回收

注:js中的内存管理是自动的,没有提供相应的api给开发人员操作

3、垃圾回收

3.1、什么是垃圾

要定义什么是垃圾,首要需要了解两个概念:

  • 可达对象

    • 可以访问到的对象就是可达对象

    • 可达的标准就是从根触发能否被找到(根:全局变量)

    • javascript中的根就可以理解为全局变量对象

image-20200531184309470

上述图内所有的对象均为可达对象,因为从根(gv)出发均可以找到。

  • 不可达对象

从根出发无法找到的对象

image-20200531184757509

当o1关系链和prev关系链不在指向obj1对象的时候,该对象就无法从根出发被找到。

可以说此时,obj1是被独立的。因为javascript将视其为垃圾,将其回收。

4、GC

4.1、GC的定义和作用

  • GC就是垃圾回收机制的简称

  • GC可以找到内存中的垃圾、并释放和回收空间

4.2、GC算法是什么

  • GC算法是一种机制,垃圾回收期的实现方式

  • 工作内容就是查找垃圾并释放空间

  • 算法就是工作时查找和回收的规则

4.3、常见的GC算法

  • 引用计数算法

  • 标记清除算法

  • 标记整理算法

  • 标记增量算法

  • 分代回收

4.4、引用计数算法

4.4.1、简介

  • 核心思想:设置引用数,判断当前引用是否为0决定是否进行回收

  • 引用计数器

  • 引用关系发生改变(±)时就修改引用数字

  • 引用数字为0 时就立即回收

ex:

var a=1  //计数器+1function fn(){    let c=a //计数器+1 此时c计数器=2    let d=1//计数器+1}fn()//执行结束,d由于只能在函数内部被调用,因此函数结束调用也就不复存在了,计数器为0。而a在全局仍有技术1,因此a不会被回收。

4.4.2、优点

  • 发现垃圾时就立即回收

  • 最大限度减少程序暂停(栈即将堆满前,立即进行一波垃圾回收)

4.4.3、缺点

  • 无法对循环引用对象进行垃圾回收

  • 时间开销大

ex:

function fn(){    const obj1={}//+1    const obj2={}//+1    obj1.name=obj2//+1    obj2.name=obj1//+1}fn()//当函数执行结束后,obj1和obj2并不会被回收,因为两者被相互引用了。引用计数算法无法对将其计数归0也就无法作为垃圾进行回收。

4.5、标记清除算法

4.5.1、简介

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

  • 遍历所有活动对象并打上标记

  • 遍历所有对象清除没有标记对象

  • 回收相应空间

ex:

image-20200531200002757

commit:

假设当前存在abc三个变量,de为ac的children。a1、b1为函数局部作用域变量。

此时标记清除算法,首先会遍历所有活动对象,ac因为有子属性所有算法会对其进行递归标记。

而a1、b1则不会被标记

第二阶段清除a1、b1

4.5.2、优点

  • 标记清除算法其实就是引用算法的升级版本,因此相对引用算法优点就是可清除循环引用对象

4.5.3、缺点

image-20200531200352751

  • 如图,进行清除的时候地址不联系,也就是空间碎片化

  • 不会立即回收垃圾对象

4.6、标记整理算法

4.6.1、简介

  • 标记整理可以看做是标记清除的增强版

  • 标记阶段的操作和标记清除的一致

  • 清除阶段会先做整理,移动再进行清除

4.6.2、优点

  • 解决了清除算法的空间碎片化问题

4.6.3、缺点

  • 不会立即回收垃圾对象

  • 移动对象位置,回收效率慢

5、V8引擎

5.1、认识V8

  • V8是一款主流的javascript引擎

  • V8采用及时编译

  • V8内存设限

    • 64位 1.5g

    • 32位 0.8g

5.2、V8的垃圾回收策略

  • 采用分代回收的思想

    • 新生代

      • 小空间(32M|16M)

      • 存活周期短

    • 老生代

      • 大空间(1.4g|0.7g)

      • 存活周期长

  • 针对不同的对象采用不同算法

5.3、V8引擎中采用GC算法

  • 分代回收

  • 空间复制

  • 标记清除

  • 标记整理

  • 标记增量

5.4、新生代对象

5.4.1、新生代对象回收实现

  • 回收过程采用复制算法+标记整理

  • 新生代内存区分为两个等大的内存空间

    • From——活动空间

    • To——空闲空间

  • 标记整理后将活动对象从From拷贝至To

  • 然后采用复制算法,From和To交换空间完成释放

5.4.2、回收细节

  • 拷贝过程中可能出现晋升

    • 晋升就是将新生代对象移动到老生代对象

    • 一轮GC后还存活的就需要晋升

    • To空间使用率超过25%的

5.5、老生代对象

5.5.1、老生代对象回收实现

  • 采用标记清除、标记整理、标记增量算法

  • 首先使用标记清除完成垃圾回收

  • 采用标记整理完成空间优化

  • 采用标记增量进行效率优化

5.5.2、与新生代对象对比

  • 新生代区域回收使用空间换时间

    • 这和其本身空间小有关系,牺牲的一点点空间和所换来的效率提升相比,微不足道

  • 老生代区域垃圾回收不适合复制算法

    • 因为其内存空间大,如果闲置着就造成了极大的空间浪费。

5.6、增量算法如何进行优化

image-20200531203512899

增量标记算法将程序执行和垃圾回收进行了分段耦合,程序分段执行,垃圾回收也同步进行,程序执行完了,垃圾回收也就结束了。

不进行增量算法

  • 优先执行程序,程序执行完之后再进行垃圾回收

  • 进行垃圾回收的时候必然会造成程序停滞

6、Performance

6.1、为什么使用performance

  • Gc的目的是为了实现内存空间的良好循环

  • 良好循环的基石是合理的使用

  • 时刻关注才能确定是否合理

  • 而performance提供多种监控方式

6.2、使用

  • 打开浏览器输入地址

  • 打开调试工具,选择performance

  • 开始录制

  • 模仿用户操作

  • 暂停录制

  • 在分析界面查看性能报告

6.3、界定内存问题的标准

  • 内存泄漏:内存使用持续升高

  • 内存膨胀:在多数设备上都存在性能问题

  • 频繁垃圾回收:通过内存变化图进行分析

6.4监控内存的几种方式

  • 浏览器任务管理器

    • shift+esc

  • timeline时序图

  • 堆快照查找分离DOM

    • 开发工具的memory,点击take snapshot获取快照即可

    • 分离DOM

      • 垃圾对象时的DOM节点

        • 脱离DOM树,且没有js使用,即为垃圾DOM

      • 分离状态的DOM节点

        • 脱离DOM树,但有js使用,即为分离DOM

  • 判断是否存在频繁的垃圾回收

    • GC工作时应用程序是停止的

    • 频繁的GC会导致应用假死

    • 用户使用过程中感知到卡顿

7、JAVASCRIPT代码优化

7.1、前提

  • js的内存管理自动完成

  • 执行引擎会使用不同的GC算法

  • 算法的目的是为了实现内存空间的良性循环

  • performance工具监测内存的变化

  • js是单线程机制的解释性语言

    • (1)源代码不能直接翻译成机器语言,而是先翻译成中间代码,再由解释器对中间代码进行解释运行; 源代码—>中间代码—>机器语言

      (2)程序不需要编译,程序在运行时才翻译成机器语言,每执行一次都要翻译一次; (3)解释性语言代表:Python、JavaScript、Shell、Ruby、MATLAB等; (4)运行效率一般相对比较低,依赖解释器,跨平台性好;

      ————————————————版权声明:本文为CSDN博主「你的代码有灵魂吗?」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/yuanziwoxin/article/details/82872792
  • JAVASCRIPT没有给开发人员相应的回收创建内存空间的API

  • 因此我们只能在代码层面上进行优化

7.2、避免使用全局变量

7.2.1、全局变量特点

  • 挂载在window或者global下

  • 全局变量至少有一个引用计数器

  • 全局变量存活更久,但持续占用内存

ex:

var i, str = ''for (i = 0; i < 1000; i++) {  str += i}​for (let i = 0; i < 1000; i++) {  let str = ''  str += i}​

7.3、避免全局查找

7.3.1全局查找相关

  • 目标变量不存在当前作用域,通过作用域链向上查找

  • 减少全局查找带来的性能浪费

  • 减少不必要的全局定义

  • 全局变量数据局部化

7.4、避免循环引用

7.5、采用字面量代替new操作

7.6、setTimeout替换setInterval

7.7、采用事件委托

7.8、合并循环变量和条件

var arrList = []arrList[10000] = 'icoder'for (var i = 0; i < arrList.length; i++) {  console.log(arrList[i])}​for (var i = arrList.length; i; i--) {  console.log(arrList[i])}

8、性能检测工具

Jsper

8.1、Jsperf的使用流程

  • github登录

  • 填写个人信息——非必填

  • 填写详细的测试用例信息(title、slug)

  • 填写Code snippets to compare(代码不够可以add)

8.2、慎用全局变量

  • 全局变量定义在顶端,是所有作用链的顶部

  • 全局执行时一直存在,直到程序退出。

  • 如果局部作用域出现了同名变量,会覆盖或污染

8.3、缓存全局变量

    function getBtn() {      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')    }​    function getBtn2() {      let obj = document      let oBtn1 = obj.getElementById('btn1')      let oBtn3 = obj.getElementById('btn3')      let oBtn5 = obj.getElementById('btn5')      let oBtn7 = obj.getElementById('btn7')      let oBtn9 = obj.getElementById('btn9')    }

8.4、通过原型添加方法

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()

8.5、避开闭包陷阱

function test(func) {  console.log(func())}​function test2() {  var name = 'lg'  return name}​test(function() {  var name = 'lg'  return name})​test(test2)

8.6、避免属性访问方法的使用

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

js中的对象

  • 对象中的所有属性都是外部可直接访问的

  • 通过属性访问会增加一次重定义,没有访问的控制力

8.7、for循环优化

var arrList = []arrList[10000] = 'icoder'for (var i = 0; i < arrList.length; i++) {  console.log(arrList[i])}​for (var i = arrList.length; i; i--) {  console.log(arrList[i])}

8.8、节点优化添加

document.createDocumentFragment()

  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)

8.9、克隆节点操作

  for (var i = 0; i < 3; i++) {      var oP = document.createElement('p')      oP.innerHTML = i       document.body.appendChild(oP)    }​    var oldP = document.getElementById('box1')    for (var i = 0; i < 3; i++) {      var newP = oldP.cloneNode(false)      newP.innerHTML = i       document.body.appendChild(newP)    }

8.10、直接量替换object操作

var a = [1, 2, 3]​var a1 = new Array(3)a1[0] = 1a1[1] = 2a1[2] = 3​

结语

文章中可能会有很多错误,如果出现了错误请大家多多包涵指正(/*手动狗头保命*/),我也会及时修改,希望能和大家一起成长。

小菜鸡的成长之路(FLOW、TS、函数编程)

小菜鸡的成长之路(ES、JS)

下一章,前端工程化