JavaScript 性能优化

1,026 阅读26分钟

1、JavaScript 性能优化 概述

随着软件开发行业不断的发展,性能优化是不可能避免的,什么的内容可以看到是性能优化?解释:本质上来说,任何一种可以提高运行效率的,降低运行的开销的的行为

在软件开发过程中,必然存在着很多值得优化的地方,特别前端开发过程中,无处不在的前端性能优化;例如请求资源所用到的网络,数据传输方式,开发过程使用的框架等;

JavaScript 语言的优化是认识内存空间的使用,垃圾回收的介绍,从而写出高效的JavaScript 代码;

性能优化阶段包含:

  • 内存管理
  • 垃圾回收与常见GC算法
  • V8引擎的垃圾回收
  • Performance工具,对内存进行监控,对当前的代码是否存需要优化

2、内存管理

  • 概念:
    • 内存:由可读写单元组成,表示一片可操作空间
    • 管理: 人为的去操作一片空间的申请、使用和释放;
    • 内存管理: 开发者主动申请空间、使用空间、释放空间
    • 管理流程: 申请-使用-释放
  • JavaScript 中的内存管理
    • 申请内存空间、
    • 使用内存空间
    • 释放内存空间 但是,EAMCscript并没有提供操作API, 所以JS语言不能像c++、c由开发主动调用相应API来完成对内存空间的管理;即使也不能阻止我们通过JS脚本演示当前在内部的一个空间生命周期是怎么完成的;(JS中由于并没有直接提共操作的API,只能在JavaScript的执行引擎遇到变量定义的时候自动分配的一个空间)
// 申请,自动分配一个空间obj
let obj ={}
// 使用,对obj空间进行一个变量定义的赋值操作
obj.name = 'lg'
//释放, 给obj赋值为null释放空间
obj = null

3、JavaScript 中的垃圾

1、什么样的内容被当作垃圾:

对前端开发来说,JavaScript中内存管理是自动的

  • 1、每当我们创建一个数组、对象、函数自动分配相应的空间,后续程序在引用过程中,无法再找到某些对象,这就会看作垃圾;(对象不再被引用时是垃圾)
  • 2、对象其实是存在的,由于某些结构或者语法的错误没有找到这个对象,也称为垃圾;(对象不能从根上访问到时是垃圾);
  • 知道是垃圾之后呢,我们的JavaScript执行引擎机就会出来工作,这个过程叫做垃圾回收;

2、JavaScript中的可达对象 (引用 根上访问)

  • 在JavaScript 中能到访问到对象,叫做可达;怎么访问: 通过具体的引用当前上下文中的作用域连查找,只要能找得到就是可达的;
  • 其他有一个可达标准就是从根出发是否能够被找到,才能是可达的;
  • 什么根呢?JavaScript中的根可以理解为是全局变量对象(全局执行上下文)

总结:JavaScript 中的垃圾回收其实就是找到垃圾,让JavaScript的执行引擎空间的释放和回收,

// 实例一
// 定一个空间,被当作obj引用,站在一个全局执行上下文下是可从根上访问的,是可达的
let obj = {name: 'xm'} 
// xm又多了一层引用,存在引用数值变化的
let ali = obj
// 空间是有两个引用,obj的xm对象被释放了,当前对象是可达的,因为ali在引用
obj = null
// 实例二
// 可达对象的示例
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)

image.png 把o1,obj2都delete的,没有办法找到obj1空间,它在这里就会当作垃圾的错操作; image.png

4、GC算法介绍

1、定义与作用

  • GC 就是垃圾回收机制的简写
  • GC可以找到内存中的垃圾、并释放和回收空间

2、GC里的垃圾什么

  • 从程序需求的考虑,程序中不再需要使用的对象,比如:某一个数据在使用完成之后,在上下文不再需要的时候,可以把当作垃圾
 function func() {
   name = 'lg'
   return `${name} is a coder`  
 }
 func()
  • 从当前程序运行过程中,变量能否还能被引用到;(不能再访问到对象)
//在函数内部定义了一个const 的变量,函数调用结束之后,在函数外部不能再去访问它,那就是垃圾
 function func() {
   const name = 'lg'
   return `${name} is a coder`  
 }
 func()

3、GC算法是什么

  • GC是一种机制,垃圾回收器可以去完成回收的工作
  • 工作的内容就是查找垃圾释放空间、回收空间
  • 算法就是工作时查找和回收所遵循的规则

4、常见GC算法

  • 引用计数:通过一个数字判断一个对象是否是垃圾
  • 标记清除:给活动对象添加一个标记,判断它是否是垃圾
  • 标记整理:
  • 分代回收:

5、引用计数算法实现原理

  • 核心思想:在内部引用计数器,判断当前引用数是否为0,来决定是否为垃圾对象,当为0 的时候,GC就会释放空间和回收空间
  • 当某一个对象。它的引用关系发生改变时,就会修改引用数字
  • 引用数字为0 时,立即回收
// 从全局上下文window可以找到user1,user2,user3,nameList,(从变量的角度出发)
// 在fn()函数定义的num1,num2,没哦域设置关键字,同样的被挂载在window下
// 所以对这些变量引用计数下肯定不是0
const user1 = {age: 11}
const user2 = {age: 22}
const user3 = {age: 33}

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

function fn() {
    // num1 = 1
    // num2 = 2
    // 添加关键字声明const
    // 当我们给fn()下添加关键字声明const,意味着只能在fun作用域起作用,
    //在全局作用域下,找不到num1,num2,这个时候它们的引用计数为0,GC开始工作,当作垃圾开始回收       
    const num1 = 1
    const num2 = 2
}
fn()
// 当脚本执行完后,user1user2user3nameList还被引用着,既不会被回收

6、引用计数算法优化缺点

  • 引用计数算法优点
    • 发现垃圾时立即回收
    • 最大限度减少程序暂停,
    • 减少程序卡顿时间
  • 引用计数算法缺点
    • 1、无法回收 循环引用 的对象
    function fn() {
      // 当代码执行完毕后,引用计数应该为0的;
      const obj1 = {}
      const obj2 = {}
      // 但是存在着一定问题, 当前GC想删除obj2,会发现obj2是指向obj1; 它们会相互有联系的,所有引用计数不是为0 的;无法回收
      obj1.name = obj2
      obj2.name = obj1
    
      return 'lg is coder'
      }
      fn()
    
    • 2、时间开销大(资源消耗较大), 当前的引用计数需要维护一个数字的变化,要时刻监听数字的修改,本身数字的修改需要时候,当有很对象需要的话,会消耗更多的时候

7、标记清除算法实现原理

  • 核心思想:将整个垃圾回收分为两个操作分标记清除来两个阶段
    • 第一阶段: 遍历所有活动对象找到标记活动对象,进行标记操作
    • 第二阶段: 遍历所有对象清除没有标记对象,找到没有标记的对象进行回收
    • 回收相应的空间

在全局的声明由A、B、C三个可达对象,找到三个可达对象会发现他它们三个会有子引用,如果发现下边有孩子,孩子还有子孩子,它会用递归的方式继续寻找可达对象,这个时候D、E,这些对象也会被做可达标记; 这个时候a1和b1放在一个局部作用域中,执行完成后就会被回收;从当前global链条下时找不到a1、b1,GC机制就会认为垃圾对象,没有做标记,就会被回收掉;如下图所示: image.png

8、标记清除算法优缺点

  • 标记清除算法优点
    • 解决对象循环引用的不能回收操作,
  • 标记清除算法缺点
    • 产生一个空间碎片的问题,然我们的空间不能最大化的使用
    • 不会立即回收垃圾对象,统一清除垃圾

9、标记整理实现原理

  • 核心思想: 标记清除的增强操作,标记阶段的操作和标记清除一致,清除阶段会执行整理,移动对象位置,能够让他们在地址上产生连续 image.png

image.png

image.png

10、标记整理优缺点

  • 减少碎片化空间
  • 不会立即回收垃圾对象

5、认识V8

1、V8概念

V8 是一款主流的 JavaScript执行引擎,日常所使用的chrome,node平台都在采用;

特点:

  • 优秀的内存管理机制
  • 采用即时编译,以前源代码转换成字节码,然后再去执行,V8直接翻译成机器码
  • V8内存设上限,64位操作系统-》不超过1.5G 32位操作系统-〉不超过800M,因为
    • V8是为浏览器而制造的,现有的内存大小对网页应用是足够使用
    • V8内部实现的垃圾回收机制,是非常合理的,因为官方做过一个测试:当我们的垃圾内存达到1.5G的时候,V8采用增量标记算法,只要消耗50毫秒,而采用非增量标记形式回收,需要1秒;

2、V8垃圾回收策略

程序使用过程中会用到很多数据,而这些数据可以分为基础原始数据对象类型数据

基础原始数据是程序语言自身控制的;

在这里提到的回收,主要指当前存活在我们堆区的对象数据,因此这个过程是离不开内存操作的,而我们也知道在V8内存是设限的,在这种情况下是怎么对垃圾回收的;

V8垃圾回收策略:

  • 采用分代回收的思想
  • 把内存规则分为两类: 新生代存储区、老生代存储区
  • 针对不同对象采用不同算法

V8内存空间分为:新生对象存储,老生对象存储,针对不同对象里采用不同算法,因此,在V8中采用了更多的GC算法

image.png

V8中常用的GC算法

  • 分代回收
  • 空间复制
  • 标记清除
  • 标记整理
  • 标记增量

3、V8如何回收新生代对象

1、V8内部内存分配

  • 基于分代回收的垃圾思想,把V8内存空间一分为二:新生代对象老生代对象
  • 小空间用于存储 新生代对象(64位-》32M|32位-》16M)
  • 新生代对象指的是存活时间较短的对象,比如:局部作用域属于新生代对象

左边存储新生代对象,右边存储老生代对象, image.png

2、新生代对象回收实现

  • 回收过程采用 复制算法 + 标记整理,如下:
    • 首先将新生代内存区分位 二个等大小空间
    • 我们将使用空间称为From状态, 空闲空间为To状态
    • 后续代码执行的时候,需要申请空间来进行使用,先将所有活动对象(所有声明对象)分配为From空间,
    • 一旦From空间使用到一定程度,就会触发GC操作;采用标记整理操作来对From空间进行活动对象标记,那么找到活动对象之后,它继续使用整理的操作把位置变得连续,也变得后续不会产生碎片化的空间,做完这些操作之后,就会把活动对象拷贝到To空间,拷贝完后,就意味着From空间的活动对象就有了一个备份,就可以对From空间进行完全释放了采用标记整理后将活动对象拷贝到To空间
    • From 与 To交换空间完胜释放: 对Form空间完全释放;

3、新生代对象回收 细节说明

  • 拷贝时发现某一个变量对象所使用的空间,在当前老生代对象也会出现,就会出先晋升的操作
  • 晋升就是 将新生代对象移动到老生代
  • 触发晋升操作:
    • 第一个:如果新生代的某些对象经过一轮GC之后还活着,就会晋升到老生代存放
    • 第二个: 如果在拷贝过程中,发现To空间的使用率超过25%(为什么选择25%:在进行回收操作的时候,最终会把From 和 To 进行交换),就会晋升到老生代存放

4、V8如何回收老生代对象

1、V8内部内存分配

  • 老生代对象存放在右侧区域
  • 64位-》1.4G, 32位-〉700M
  • 老生代就是指 存活时间较长的对象,比如: 全局作用域, 闭包作用域

2、老生代对象回收实现

  • 回收过程采用: 标记清除、标记整理、增量标记算法
    • 首先先使用标记清除完成对垃圾空间的释放和回收
    • 如果发现想把新生代的内容 往 老生代区域移动的时候,而发现老生代存储区域空间 不足以 存放新生代的移过来的对象,这种情况下,就会触发标记整理,把之前的碎片空间进行整理回收,就会有更多的空间存放新生代的内容;触发采用标记整理进行空间优化
    • 采用增量标记进行效率优化

3、老生代对象回收 细节对比

  • 新生代垃圾回收使用空间换时间,因为它采用的是复制算法,意味着每时每刻都会空闲空间的存在;但由于新生代本身的空间很小,分出来的空间更小,这部分空间浪费相当于它所带来的时间提升是微不足道
  • 老生代垃圾回收不适合复制算法,因为老生代的存储区域是很大的,如果说一分为二,基本上是浪费的;另外一个是老年代存放的数据是比较多的,所有在复制过程中消耗的时候就会多

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

当垃圾回收进行工作的时候,它其实就会阻塞当前Javascript程序执行的;有一个空档期,例如程序执行完成后会停下来,会执行当前的回收操作,

标记增量是指: 将当前一整段的垃圾回收操作分为多个小部组合去完成当前整个回收,从而去取代先前一口气的垃圾回收操作;实现垃圾回收和程序执行交替去完成;所带来的时间消耗更合理; image.png

6、Performance 介绍

1、为什么使用Performance

  • GC的目的为了实现内存空间的良性循环使用
  • 良性循环的基石是合理使用,由于ECMAScirpt没有给提供操纵内存空间的API;
  • 判断内存是否合理: 想办法时刻关注到当前内存的变化,
  • 所有就提供了一款工具Performance监控方式,来在我们程序中运行过程中的监控操作

总结: 通过Performance时刻监控内存,可以在程序内存中出现问题可以想办法定位到问题代码块;

2、 Performance 使用步骤

  • 打开浏览器输入目标网址,
  • 进入开发人员工具面板,选择性能
  • 开启录制功能,访问具体界面(网址)
  • 充当用户:执行用户行为,一段时间后停止录制
  • 得到报告: 分析网页总记录的内存信息

3、 内存问题的体现

当我们程序出现问题如何体现

  • 外在表现
    • 页面出现延迟加载或经常性暂停,判断内存有问题的,当前GC存在频繁的垃圾回收操作相关;肯定是程序中代码出现问题
    • 页面持续性出现糟糕的性能;认为存在着内存膨胀,所谓的内存膨胀就是当前界面为了达到使用速度可能会申请 一定的内存空间,但是内存空间大小远超过了当前设备本身所能提供的一个大小;
    • 页面的性能随时间延长越来越差;就是内存泄漏,由于某些代码的出现,随着时间的挣扎,让我们的内存空间越来越小; 总结 :出现这些问题,结合Performance工具进行 内存分析操作,从而定位到有问题的代码; 进行修改后, 让我们当前的程序能够流畅些;

4、 监控内存的几种方式

1、当我们内存出现问题的时候,一般归纳为三种情况:
  • 内存泄漏
  • 内存膨胀
  • 频繁的立即回收
2、界定内存问题的标准
  • 内存泄漏: 内存使用持续升高,判断当前程序中执行过程中的内存走势图,如果图上内存一直持续升高的,但是整个过程没有下降的节点,意味着我们程序是存在着内存泄漏的;可以定位代代码
  • 内存膨胀:在多数设备上都存在性能问题;判断当前程序的问题还是设备的问题,应该多做的测试;
  • 频繁的立即回收: 通过内存变化图进行分析;通过界面没有 办法界定的;
3、监控内存的几种方式
  • 浏览器任务管理器: 以数值的形式将当前的应用程序内存的变化体现出来
  • Timeline时序图记录: 把当前应用程序执行的内存走势都 以时间点的方式记录下来
  • 堆快照查找分离DOM: 针对性查找当前的界面中是否有分离的DOM,分离DOM存在就是内存泄漏
  • 判断是否存在频繁垃圾回收:借助不同的工具来获取当前内存走势图,然后进行时间段的分析,从而得到判断;

4、 浏览器任务管理器监控方式

  • 打开浏览器任务管理器: shift+esc image.png
  • 可以定位到当前执行的脚本,(默认情况下是没有最后一列Javascript内存这一项,可以右击当前执行脚本--》JavaScript内存
  • 关 注内存 和 Javascript内存,
    • 第一列内存: 其实就是原生内存;简单理解:当前界面里会有很多的DOM节点,而这个内存指的就是DOM节点所占据的内存;如果说这个数值一致在持续的增大,说明了我们界面在不断的创建新DOM;(频繁的操作DOM)
    • Javascript内存: 表示的是JS的堆,需要关注的是小括号里面的值,表示的是当前界面当中,所有可达对象增大使用的内存大小,如果说这个数值一直增大,意味着当前界面中,要么在创建新对象,要么当前现有对象在不断增长,说明内存一只往上走,没有GC消耗,内存是有问题的;
  • 浏览器任务管理器缺点
    • 只能帮助我们发现有没有问题,只能判断是否有问题,定位不到问题 image.png

4、Timeline 记录内存 监控方式

通过时间线记录变化的方式,更精确定位到代码的问题

  • 调出当前的工具,选择性能,点击计时的操作,开始录制;
  • JS堆走势图:有涨有降,涨是申请内存,降是销毁内存;直线往上走意味着内存的消耗,代码存在问题,内存泄漏;定位:定位时间点,可以看见界面的变化;
  • Timeline优点: 可以帮我定位到问题 image.png

5、堆快照查找分离DOM 监控

找到当前的JS堆,对它进行照片留存,可以看到所有信息,这就是监控的由来;堆快照查找 就像是分离DOM

DOM节点的形态分为:

  • 垃圾对象:如果这个节点从我们当前的DOM树上进行了脱离,而且在JS代码当中,也没有人再引用的DOM节点,是垃圾对象
  • 分离DOM:当前的DOM节点只是从当前的DOM树上脱离了,但是在我们JS代码中还有人在引用它,称之为分离DOM,在界面是看不见,在内存中占据空间的,会造成内存泄漏; 通过堆快照的功能找出来,针对这些代码进行修改,释放空间;
1、 什么是分离DOM
  • 界面元素存活在DOM树上
  • 垃圾对象时到DOM节点
  • 分离状态的DOM节点 image.png

5、判断是否存在频繁GC

1、为什么确定频繁垃圾回收
  • GC工作时应用程序是停止的
  • 频繁且过长的GC会导致应用假死
  • 用户使用感知应用卡顿
2、确定频繁垃圾回收的方式
  • Timeline中频繁的上升下降
  • 任务管理器中数据频繁的增加减少

7、V8引擎工作流程

V8: 本身也是应用程序,也是JS的执行环境;把V8看成浏览器的组成部分,用来解析和编译所书写的JS代码,在内部也存在着很多字模块; image.png

V8引擎是浏览器的渲染引擎里的一个JS执行代码组成部分

1、Scanner

Scanner是一个扫描器;对于我们村文本的JS代码进行一个词法分析,它会去把代码分析成不同的tokens;

2、Parser

Parser是一个解析器, 解析过程就是语法分析的过程,它会把词法分析结果当中的tokens,然后转换成抽象的语法树,同时进行语法校验,如果有语法错误,直接抛出错误;

3、预解析优点

image.png

4、全量解析

image.png image.png

4、Ignition

Ignition是V8提供的一个解释器

作用: 把生成的抽象语法树AST,转为字节码,同时还收集下一个编译阶段所需的信息;

5、TurboFan是V8提供的编译器模块
  • 之前得到的是字节码,最终的执行的是机器码,所欲它利用上一环节中的所收集到的信息,把字节码转换为汇编代码,最后开始代码执行;

8、堆栈操作

  • JS执行环境: V8 -》机器码

  • 1、执行环境栈(ECS): 浏览器在渲染界面的时候会在计算机内存中分配一块空间,专门用来执行JS代码;而这个栈内存就是执行环境栈,

    • 但是不能把所有的代码放在一块,不同的代码之间,保存相互的独立,不能互相影响比如: 全局上下中有个变量a, 局部作用域有个变量a,这两个a如何做到隔离的;这是需要执行上下纹
  • 2、执行上下文(EC)它是管理不同的区;``全局的上下文用来管理全局代码执行,私有上下文用来管理局部代码执行;每个执行上下文执行进栈操作就可以了;

    • 以全局上下文为例分析,在全局执行的上下文中同时存在着多个声明a,b,那这些存在哪里,底层就存在着一个全局变量对象
  • 3、VO(G), 全局变量对象: 所有的变量声明都存在这里,全局代码就可以进栈执行了; 在栈底 它永远都会有一个EC(G)全局执行上下文,而代码的执行步骤针对全局:

    • GO 全局对象,它并不是VO(G)但是它也是一个对象,也会有一个内存空间地址;在浏览器刚要加载某一个界面的时候,会单独分配一个独立空间,这片空间叫做GO;因为有地址就可以访问,JS在VO(G)中准备一个变量叫window
  • 4、AO 私有变量对象:一个存放当前上下文的变量对象,

总结 值类型

  • 基本数据类型是按值进行操作,
  • 基本数据类型值是存放在 栈区
  • 无论我们当前看到的栈内存,还是后续引用数据类型会使用的堆内存都是属于计算机内存image.png

9、引用类型堆栈操作

  • 引用类型放在堆内存中; 02-对象堆栈执行.png

10、函数堆栈处理

函数创建
  • 创建函数和创建变量类似,函数名此时就可以 看做是一个变量名存放在VO中,同时它的值就是当前函数对应的内存地址;
  • 函数创建时,函数本身是一个对象,会单独开辟一个堆内存用于存放 函数体(字符串形式代码),当前内存地址也会有一个16进制值存在栈区
  • 创建函数的时候,它的作用域[[scope]]就已经确定了(创建函数时所在的执行上下文)
  • 创建函数之后会将它的内存地址存放在栈区与对应的函数名进行关联
函数执行目的

就是为了将函数对应的对内存里的字符串形式代码进行执行。代码在执行的时候肯定需要有一个环境,此时就意味着函数在执行的时候就会生成一个新的上下文来管理函数体当中的代码

函数执行时做的事情
  • 函数执行时会形成一个全新私有上下文,它里面有一个AO用于管理这个上下文的变量
  • 01、确定作用域连:<当前执行上下文、上级执行上下文>
  • 02、确定this ----》 window
  • 03、初始化argument对象
  • 04、形参赋值: obj= arr其他就是变量的声明
  • 05、变量提升
  • 06、执行代码

03-函数堆栈执行.png

11、闭包堆栈处理

  • 闭包是一种机制,通过私有上下文来保护当中变量的机制
  • 也可以认为当我们创建的某一个执行上下文不被释放的时候;就形成了闭包
  • 保存当前局部变量,不受干扰,保护数据,在外部被使用

04-闭包与堆栈执行.png

12、JSBench使用

  • 功能: 它是一个在线测试JS执行效率的网站,目前已经停止维护;
  • 打开方式: JSBench.me

13、变量局部化 性能优化 必记

  • 通过把一些变量进行局部化处理,从而提高代码执行的性能
  • 如果定义变量的时候,能够定义成局部的作用域,就放在私有的上下文中,因为可以提高代码的速度(减少了数据访问时需要查找的路径
  • 数据的存取和读取希望能够在作用域上减少访问层级,从而提高代码的执行速度

14、缓存数据 性能优化 必记

  • 功能: 提高代码的执行数据
  • 缓存数据: 对于需要多次使用的数据进行提前保存,后续进行使用;
  • 编写代码时,减少不必要的语句声明和语句数,因为执行代码之前有一个词法分析和语法分析,需要一定的时间
  • 把本该属于其他堆空间的数据缓存到了当前的栈或者执行上下文中,可以直接从自己的作用域中查找;(作用域查找更快)
// 缓存数据
var oBox = document.getElementById('skip')
// 假设在函数体中多次使用,可以缓存起来
// 没有缓存
//function hashClassName(ele, cls){
//    console.log(ele.className)
  //  return ele.className == cls
//}
// console.log(hashClassName(oBoc, 'skip'))

// 有缓存
function hashClassName(ele, cls){
    var clsName = ele.className // 缓存数据
    console.log(clsName)
    return ele == cls
}
console.log(hashClassName(oBoc, 'skip'))

14、减少访问层级 性能优化 必记

  • 功能: 提高代码执行效率
var obj ={
    age: 18
    methods: {
        m1: {
            name: 'ls',
            time: 100
        },
        me: {
            name: 'zs',
            time: 200
        }
    }
}

15、防抖与节流 性能优化 必记

1、为什么需要防抖和节流

当前的web前端大多数是运行在浏览器平台,就会有很多人机交互的操作,如果疯狂的点击按钮,背后的代码有相应的事件监听,一定会去执行,而且浏览器本身有监听机制,在一定时间范围内,如果监听到了事件就会执行,执行代码内存空间就会被占用掉,浏览器本身也是一个应用,本身的占用内存也是有限的,就会出现高频次触发事件的场景,所以这这种事件不需要事件处理函数多次执行;所以进行防抖和节流

2、场景

  • 滚动事件
  • 输入的模糊匹配
  • 轮播图的切换
  • 点击操作(人机交互) 总结:浏览器默认情况下都会有自己的监听事件间隔(4~6ms),如果检测到多次事件的监听执行会造成不必要的资源浪费,需要一种机制,叫做防抖和节流

3、防抖和节流的定义

  • 前置的场景: 界面有一个按钮,我们可以连续多次点击
  • 防抖的定义: 对于这个高频的操作来说,我们只希望识别一次点击,可以认为是第一次或者最后一次点击
  • 节流的定义:对于高频操作,我们可以自己设置频率,让本来会执行很多次的事件触发,按着我们定义的频率减少触发的次数;

4、防抖函数的实现

<<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0, user-scalable=no">
    <title>防抖的函数实现</title>
</head>
  <body>
    <button id="btn">点击</button>
    <script>
      var oBtn = document.getElementById('btn')
      // oBtn.onclick = function() {
      //   console.log('点击了')
      // }
      /*
      *  handle:最终需要执行的事件监听,
      *  wait: 时间触发之后开始执行的时间. 
      *  immediate: 控制执行的第一次还是最后一次, false 为执行最后一次
      */ 
      function myDebounce(handle, wait, immediate) {
         // 参数类型判断及默认值处理
          if (typeof handle !== 'function') throw new Error('handle must be an function')
          if (typeof wait === 'undefined') wait = 300
          if (typeof wait === 'boolean') {
            immediate = wait
            wait = 300
          }
          if (typeof immediate !== 'boolean') immediate = false
          // 所谓的防抖效果我们想要实现的就是有一个“人”,可以管理handle的执行次数
          // 如果我们想要执行最后一次,那就意味着无论我们当前点了多少次,前面的 N-1次都无效
          let timer = null // 返回结果
          return function proxy(...args) {
            let self = this // 保存this
            let init = immediate && !timer 
            clearTimeout(timer) // 点击第二次的时候,清除timer,取最后一次的点击
            // 如果timer不为null,执行下面语句
            timer = setTimeout(() => {
              timer = null
              !immediate ? handle.call(self, ...args): null // 改变this的指向;
            },wait) 
            // 如果当前传递进来的true,表示需要立即执行
            // 如果想要实现只在第一次追星,那么可以添加timerw为null作为判断
            // 因为只要timer为Null,就意味着没有第二次点击了
            init ? handle.call(self, ...args): null
          }
        }
      // 定义事件执行函数
      function btnCLick (ev) {
        console.log('点击了', this, ev)
      }
      // btnCLick
      oBtn.onclick = myDebounce(btnCLick, 200, false)
      // oBtn.onclick = btnCLick() // this ev
    </script>
  </body>
</html>

5、节流函数的实现

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>节流函数实现</title>
  <style>
    body {
      height: 5000px;
    }
  </style>
</head>

<body>
  <script>
    // 节流:我们这里的节流指的就是在自定义的一段时间内让事件进行触发

    function myThrottle(handle, wait) {
      if (typeof handle !== 'function') throw new Error('handle must be an function')
      if (typeof wait === 'undefined') wait = 400

      let previous = 0  // 定义变量记录上一次执行时的时间 
      let timer = null  // 用它来管理定时器

      return function proxy(...args) {
        let now = new Date() // 定义变量记录当前次执行的时刻时间点
        let self = this
        let interval = wait - (now - previous)

        if (interval <= 0) {
          // 此时就说明是一个非高频次操作,可以执行 handle 
          clearTimeout(timer)
          timer = null
          handle.call(self, ...args)
          previous = new Date()
        } else if (!timer) {
          // 当我们发现当前系统中有一个定时器了,就意味着我们不需要再开启定时器
          // 此时就说明这次的操作发生在了我们定义的频次时间范围内,那就不应该执行 handle
          // 这个时候我们就可以自定义一个定时器,让 handle 在 interval 之后去执行 
          timer = setTimeout(() => {
            clearTimeout(timer) // 这个操作只是将系统中的定时器清除了,但是 timer 中的值还在
            timer = null
            handle.call(self, ...args)
            previous = new Date()
          }, interval)
        }
      }

    }

    // 定义滚动事件监听
    function scrollFn() {
      console.log('滚动了')
    }

    // window.onscroll = scrollFn
    window.onscroll = myThrottle(scrollFn, 600)
  </script>
</body>

</html>

08-节流原理.png

6、减少判断层级 性能

减少判断层级对性能的影响,具体的来收,就是在写代码的层级出现多层级嵌套的场景,而往往if,else多层嵌套的时候,都可以提前return无效的条件,达到无效层级的效果;(减少了算法的改变,没有多层嵌套if;有明确条件判断,使用switch,case,利用维护;if,else更多用在区间判断)

image.png

7、减少循环题活动

放在循环体中往往都是固定执行的内容,循环的内容越多,说明执行效率越慢;反之就会越高。所以,把每次循环都要用到的、都要操作的一些数据值不发生改变的都把它抽离出循环体外去完成(类似于数据缓存)

16 字面量与构造式