文章目录

  • 一、性能优化介绍
  • 二、JavaScript 内存管理(Memory Management)
    • 2.1 内存管理介绍
    • 2.2 JavaScript 中的内存管理
  • 三、JavaScript 中的垃圾回收
  • 四、GC 算法介绍
    • 4.1 GC 里的垃圾是什么
    • 4.2 GC 算法是什么
    • 4.3 常见的 GC 算法
    • 4.4 引用计数算法
      • 4.4.1 引用计数算法实现原理
      • 4.4.2 引用计数算法优点
      • 4.4.3 引用计数算法缺点
    • 4.5 标记清除算法
      • 4.5.1 标记清除算法实现原理
      • 4.5.2 标记清除算法优点
      • 4.5.3 标记清除算法缺点
    • 4.6 标记整理算法
      • 4.6.1 标记整理算法实现原理
      • 4.6.2 标记整理算法优点
      • 4.6.3 标记整理算法缺点
  • 五、V8 引擎的垃圾回收
    • 5.1 认识 V8
    • 5.2 V8 垃圾回收策略
    • 5.3 V8 中常用 GC 算法
    • 5.4 V8 如何回收新生代对象
      • 5.4.1 V8 内存分配
      • 5.4.2 新生代对象说明
      • 5.4.3 新生代对象回收实现
      • 5.4.4 回收细节说明
    • 5.5 V8 如何回收老生代对象
      • 5.5.1 老生代对象说明
      • 5.5.2 老生代对象回收实现
    • 5.6 新生代与老生代回收对比
    • 5.7 标记增量如何优化垃圾回收
  • 六、Performance 工具介绍
    • 6.1 为什么使用 Performance
    • 6.2 Performance 使用步骤
    • 6.3 内存问题的体现(外在表现)
    • 6.4 监控内存的几种方式
      • 6.4.1 界定内存问题的标准
      • 6.4.2 监控内存的几种方式
      • 6.4.3 任务管理器监控内存
      • 6.4.4 Timeline 时序图记录内存
      • 6.4.5 堆快照查找分离 DOM
    • 6.5 判断是否存在频繁 GC
      • 6.5.1 为什么要确定频繁垃圾回收
      • 6.5.2 确定频繁垃圾回收的方法
  • 七、V8 引擎工作流程
  • 八、堆栈处理
    • 8.1 简单模型的执行堆栈
    • 8.2 对象模型的执行堆栈
    • 8.3 函数模型的执行堆栈
    • 8.4 闭包堆栈处理
    • 8.5 闭包与垃圾回收
    • 8.6 闭包应用场景分析 - 循环添加事件
      • 8.6.1 循环添加事件实现
      • 8.6.2 事件添加底层分析
  • 九、代码优化
    • 9.1 测试 JavaScript 性能
    • 9.2 变量局部化
    • 9.3 缓存数据
    • 9.4 减少访问层级
    • 9.5 防抖与节流
      • 9.5.1 需求场景
      • 9.5.2 防抖函数实现
      • 9.5.3 节流函数实现
    • 9.6 减少判断层级
    • 9.7 减少循环体活动
    • 9.8 字面量与构造式

一、性能优化介绍

  • 性能优化是不可避免的
  • 任何一种可以提高运行效率,降低运行开销的行为都可以看做是一种优化操作
  • 无处不在的前端性能优化:请求资源用到的网络、数据的传输方式、开发过程中使用到的框架等
  • 接下来我们探讨的核心为 JavaScript 语言的优化

二、JavaScript 内存管理(Memory Management)

2.1 内存管理介绍

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

2.2 JavaScript 中的内存管理

  • 申请内存空间
  • 使用内存空间
  • 释放内存空间

由于 ECMAScript 中并没有提供相应的操作 API,所以 JS 语言不能像 C 或者 C++ 语言那样由开发者主动调用相应的 API 来完成空间的管理。不过,即使如此,它也不能影响我们通过 JS 脚本演示当前一个内存空间的生命周期是怎样完成的

// 申请
let obj = {};// 使用
obj.name = "lg";// 释放
obj = null;

三、JavaScript 中的垃圾回收

  • JavaScript 中的内存管理是自动的
  • 对象不再被引用时是垃圾
  • 对象不能从根上访问到时是垃圾

JavaScript 中的可达对象

  • 可以访问到的对象就是可达对象(引用、作用域链)
  • 可达的标准就是从根出发是否能够被找到
  • JavaScript 中的根就可以理解为是全局变量对象

JavaScript 中的引用与可达

示例一

let obj = { name: "zs" }; // 空间被引用且 obj 可以从根上被找到let ali = obj; // zs 所处的空间多了一处应用obj = null; // obj 引用被断开,但是 zs 还是一个可达对象,因为 ali 还在引用console.log(ali); // { name: 'zs' }

示例二

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 就是垃圾回收机制的简写
  • GC 可以找到内存中的垃圾、并释放和回收空间

4.1 GC 里的垃圾是什么

  • 程序中不再需要使用的对象(程序需求角度)
function() func() {name = 'lg' // 当函数调用完成后,name 则不再需要使用return `${name} is a coder`
}
func()
  • 程序中不能再访问到的对象(程序运行角度)
function() func() {// 添加了声明变量关键字,当函数调用结束后,外部就不能再访问到 nameconst name = 'lg'return `${name} is a coder`
}
func()

4.2 GC 算法是什么

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

4.3 常见的 GC 算法

  • 引用计数
  • 标记清除
  • 标记整理
  • 分代回收

4.4 引用计数算法

4.4.1 引用计数算法实现原理

其思路是对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存

  • 核心思想:设置引用数,判断当前引用数是否为 0
  • 引用计数器
  • 引用关系改变时修改引用数字
  • 引用数字为 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 = 1num2 = 2
}fn()

接下来做一些修改

  • 加上了关键字的声明过后就意味着 num1 和 num2 只能在作用域内起效果了
  • 一旦 fn() 函数调用执行结束之后,从外部全局的角度出发就不能再找到 num1 和 num2,此时它们身上的引用计数就会回到 0
  • 只要是 0 的情况下,GC 就会立即开始工作,将它们当作垃圾进行对象回收
  • nameList 里面都指向了 user1、user2、user3 三个对象空间,所以即使脚本执行完以后,它们都还被引用,所以此时的引用计数器不为 0,此时就不会被当做垃圾回收
const user1 = {age: 11}
const user2 = {age: 22}
const user3 = {age: 33}const nameList = [user1.age, user2.age, user3.age]function fn() {const num1 = 1const num2 = 2
}fn()

4.4.2 引用计数算法优点

  • 发现垃圾时立即回收(可以即时回收垃圾对象)
  • 最大限度减少程序暂停(应用程序在执行过程中,必然会对内存进行一个消耗,而当前的执行平台内存是有上限的,所以内存肯定有占满的时候。由于引用计数算法是时刻监控引用数值为 0 的对象,所以我们就可以认为,当它发现内存即将爆满时,引用计数就会立马去找到数值为 0 的对象空间,然后对其进行释放,保证内存不会有占满的时候)

4.4.3 引用计数算法缺点

  • 无法回收循环引用的对象
  • 时间开销大(资源消耗较大)

循环引用,所谓的循环引用,就是对象 A 有一个指针指向对 B ,而对象 B 也引用了对象 A,这意味着它们的计数都是2,它们的计数永远不会为0,会导致大量内存永远不会被释放

function fn() {const obj1 = {} // 虽然在全局无法找到const obj2 = {}obj1.name = obj2 // 但是此作用域内还互相引用,引用计数不为 0obj2.name = obj1return 'lg is a coder'
}fn()

4.5 标记清除算法

JavaScript 最常用的垃圾回收策略是标记清除(mark-and-sweep)

4.5.1 标记清除算法实现原理

  • 核心思想:分标记和清除两个阶段完成
  • 遍历所有对象,找到、标记活动对象(可达对象)
  • 遍历所有对象,清除没有标记的对象,同时将设置的标记抹掉,便于 GC 下次还能正常工作
  • 回收相应的空间到空闲链表

4.5.2 标记清除算法优点

  • 相对于引用计数算法,解决对象循环引用的回收操作

4.5.3 标记清除算法缺点

  • 回收到空闲链表的地址不连续,浪费空间(空间碎片化)
  • 不会立即回收垃圾对象(清除时会阻塞程序的执行)

4.6 标记整理算法

4.6.1 标记整理算法实现原理

  • 标记整理可以看做是标记清除的增强操作
  • 标记阶段的操作和标记清除一致
  • 清除阶段会先执行整理,移动对象位置



4.6.2 标记整理算法优点

  • 回收到的空间基本连续,减少碎片化空间

4.6.3 标记整理算法缺点

  • 不会立即回收垃圾对象(清除时会阻塞程序的执行)

五、V8 引擎的垃圾回收

5.1 认识 V8

  • V8 是一款主流的 JavaScript 执行引擎(Chrome 浏览器、Node 平台)
  • V8 采用即时编译(直接将源码翻译为机器码)
  • V8 内存设限
    • 在 64 位的操作系统下,不超过 1.5G
    • 在 32 位的操作系统下,不超过 800M

5.2 V8 垃圾回收策略

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

5.3 V8 中常用 GC 算法

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

5.4 V8 如何回收新生代对象

5.4.1 V8 内存分配

5.4.2 新生代对象说明

  • V8 内存空间一分为二
  • 小空间用于存储新生代对象
  • 在 64 位操作系统中大小为 32M,在 32 位操作系统中大小为 16M
  • 新生代指的是存活时间较短的对象

5.4.3 新生代对象回收实现

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

5.4.4 回收细节说明

  • 拷贝过程中可能会出现晋升
  • 晋升就是将新生代对象移动至老生代
  • 一轮 GC 还存活的新生代需要晋升
  • To 空间的使用率超过 25% 需要晋升

5.5 V8 如何回收老生代对象

5.5.1 老生代对象说明

  • 老生代对象存放在右侧老生代区域
  • 64 位操作系统 1.4G,32 位操作系统 700M
  • 老生代对象就是指存活时间较长的对象(例如:在全局对象下所存放的变量、闭包)

5.5.2 老生代对象回收实现

  • 主要采用标记清除、标记整理、增量标记算法
  • 首先使用标记清除完成垃圾空间的回收
  • 采用标记整理进行空间优化(老生代空间不足以支持新生代晋升时会触发)
  • 采用增量标记进行效率优化

5.6 新生代与老生代回收对比

  • 新生代区域垃圾回收使用空间换时间(复制算法)
  • 老生代区域垃圾回收不适合复制算法(因为空间太大,会造成空间浪费;存储的对象数据比较多,复制消耗的时间多)

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

当垃圾回收进行工作时,会阻塞 JavaScript 程序的执行,所以这里会有一个空档期。而所谓的标记增量,就是将当前一整段的垃圾回收拆分成一小步,组合完成垃圾回收,从而替代我们之前一口气做完的垃圾回收。这样做的好处就是可以实现垃圾回收与程序执行交替完成

六、Performance 工具介绍

6.1 为什么使用 Performance

通过 Performance 时刻监控程序运行过程中内存的变化。在内存出现问题时,可以帮助我们定位到出现问题的地方

  • GC 的目的是为了实现内存空间的良性循环
  • 良性循环的基石是合理使用
  • 而 ECMAScript 没有提供操作内存空间的 API
  • 时刻关注才能确定是否合理
  • Performance 提供多种监控方式

6.2 Performance 使用步骤

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

6.3 内存问题的体现(外在表现)

网络正常的前提下:

  • 页面出现延迟加载或经常性暂停
  • 页面持续性出现糟糕的性能
  • 页面的性能随时间延长越来越差

6.4 监控内存的几种方式

6.4.1 界定内存问题的标准

  • 内存泄露:内存使用持续升高
  • 内存膨胀:在多数设备上都存在性能优化
    • 当前应用本身为了达到最优的效果需要很大的内存空间
    • 有可能是由于当前设备本身硬件不支持
  • 频繁垃圾回收:通过内存变化图进行分析

6.4.2 监控内存的几种方式

  • 浏览器任务管理器
  • Timeline 时序图记录
  • 堆快照查找分离 DOM
  • 判断是否存在频繁的垃圾回收(获取内存走势图进行分析)

6.4.3 任务管理器监控内存

  • 通过快捷键 Shift + Esc 调出当前浏览器自带的任务管理器(Mac OS 环境需要在浏览器工具选项中打开)
  • 定位到当前正在执行的脚本
  • 可以对其右击打开 JavaScript 内存选项(默认关闭,Mac OS 环境需要在最顶部的列标题栏右击打开)
  • 选项内存表示原生内存(DOM 节点所占据的)
  • 选项 JavaScript 内存表示 JS 堆,实时内存表示界面所有可达对象正在使用的内存大小
  • 如果 JS 实时内存一直增大就意味着内存是有问题的(只能发现问题,无法定位问题)

6.4.4 Timeline 时序图记录内存

Timeline 是 Google的 chrome 浏览器中的一个开发者工具,它有助于前端开发者来分析页面的解析、脚本运行以及渲染、布局的情况,从而帮助开发者去优化页面的性能

<!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></head><body><button id="btn">Add</button><script>const arrList = [];function test() {for (let i = 0; i < 100000; i++) {document.body.appendChild(document.createElement("p"));}arrList.push(new Array(100000).join("x"));}document.getElementById("btn").addEventListener("click", test);</script></body>
</html>

6.4.5 堆快照查找分离 DOM

什么是分离DOM

  • 界面元素存活在 DOM 树上
  • 垃圾对象时的 DOM 节点
    • 如果一个节点从当前 DOM 树脱离,而且 JS 代码中没有引用此节点,就称其为垃圾 DOM
  • 分离状态的 DOM 节点
    • 如果一个节点从当前 DOM 树脱离,但是 JS 代码还有引用此节点,就称其为分离 DOM
  • 通过分析用户行为执行前后,对比所拍摄快照中是否存在 detached 来确定脚本中是否存在分离 DOM

分离 DOM 在界面上是看不见的,但其在内存里占据空间,这种情况下就是一种内存泄露。因此我们可以通过堆快照的功能把它们找出来,只要可以找到就可以回到代码针对性的清除,从而使内存得到释放,脚本的执行更加迅速

<!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></head><body><button id="btn">Add</button><script>var tmEle;function fn() {var ul = document.createElement("ul");for (var i = 0; i < 10; i++) {var li = document.createElement("lo");ul.appendChild(li);}tmEle = ul;}document.getElementById("btn").addEventListener("click", fn);</script></body>
</html>

6.5 判断是否存在频繁 GC

6.5.1 为什么要确定频繁垃圾回收

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

6.5.2 确定频繁垃圾回收的方法

  • Timeline 中频繁的上升下降(蓝色线)
  • 任务管理器中数据频繁的增加减小(DOM 节点内存大小和 JS 内存大小浮动变化)

七、V8 引擎工作流程

  • Scanner 是一个扫描器:词法分析,对纯文本的 JavaScript 代码进行分析,分析成不同的 tokens

分析前

const username = 'alishi'

分析后(仅仅是用数组展示一下,并不代表 JS 里面存在这个东西)

[{"type": "Keyword","value": "const",},{"type": "Identifier","value": "username",},{"type": "Punctuator","value": "=",},{"type": "String","value": "alishi",},
]
  • Parser 是一个解析器:语法分析过程,会把词法分析的 tokens 转换成 AST 抽象的语法树,同时也会做语法校验

  • PreParser 是预解析

    • 优点

      • 跳过未被使用的代码
      • 不生成 AST,创建无变量引用和声明的 scopes
      • 依据规范抛出特定错误
      • 解析速度更快
  • 全量解析

    • 解析被使用的代码
    • 生成 AST
    • 构建具体 scopes 信息,变量引用、声明等
    • 抛出所有语法错误
// 声明时未调用,因此会被认为是不被执行的代码,进行预解析
function foo() {console.log("foo");
}// 声明时未调用,因此会被认为是不被执行的代码,进行预解析
function fn() {}// 函数立即执行,只进行一次全量解析
(function bar() {})();// 执行 foo,那么需要重新对 foo 函数进行全量解析,此时 foo 函数被解析量两次
foo();
  • lgnition 是 V8 提供的一个解释器:把之前所生成的 AST 抽象语法树转为字节码,同时还会收集下个编译阶段所需要的信息
  • TurboFan 是 V8 提供的编译器模块:把字节码转换成汇编代码,就可以开始代码执行了,也就是堆栈执行过程

八、堆栈处理

JavaScript 代码在执行时,浏览器在内存层面上所做的一些底层处理

8.1 简单模型的执行堆栈

  • JS 代码的运行,是需要先向内存申请存储空间,然后存入内存后运行的

  • JS 申请的这部分内存,我们可以叫做执行环境栈(ECStack,execution context stack)

  • 在执行环境栈中,会先创建全局代码执行所需要的空间,这个空间叫做全局执行上下文 (也被称为 ECG)

  • 全局作用域中的声明和变量,会被存入全局变量对象中,也就是在全局执行上文中的一个空间,也被叫做 VO(G)

var i = 100
var g = i
g = 101
console.log(i)// 基本数据类型是按值进行操作
// 基本数据类型值是存放在 栈区的
// 无论我们当前看到的栈内存,还是后续引用数据类型会使用的堆内存都属于计算机内存
/* GO:全局对象,并不是 VO(G),但是它也是一个对象(setInterval、setTimeout等),
因此它也会有一个内存的空间地址。因为有地址就可以对其进行访问,JS 会在 VO(G) 当中准备一个
变量叫 window */

编译阶段

执行阶段

8.2 对象模型的执行堆栈

var i = {x:66}
var g = i
g.y = 77
console.log(i.y)

编译阶段

执行阶段

8.3 函数模型的执行堆栈

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

编译阶段

执行阶段 01

执行阶段 02

  • 函数执行,目的就是为了将函数所对应的堆内存中的字符串形式代码进行执行
  • 代码在执行的时候需要一个环境,此时就意味着函数在执行的时候会生成一个新的执行上下文来管理函数体中的代码
  • 函数执行时做的事情
    • 01 确定作用域链:<当前执行上下文、上级作用域所在的执行上下文>
    • 02 确定 this
    • 03 初始化 arguments 对象
    • 04 形参赋值:变量声明(参数就是一个局部变量)
    • 05 变量提升(AO)
    • 06 代码执行

8.4 闭包堆栈处理

MDN-闭包

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被 引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来

  • 闭包是一种机制,通过私有上下文来保护当中变量的机制
  • 函数调用形成一个全新的私有上下文,在函数调用之后当前上下文不被释放就是闭包
  • 保护:当前上下文中的变量与其它的上下文中变量互不干扰
  • 保存:当前上下文中的数据(堆内存)被当前上下文以外的上下文中的变量所引用,这个数据就保存下来了

闭包解析

闭包执行

8.5 闭包与垃圾回收

  • 浏览器都自带有垃圾回收(内存管理:以 V8 为例)
  • 栈空间、堆空间
  • 堆:当前堆内存如果被占用,就不能被释放掉。但是我们如果确认后续不再使用内存里的数据,也可以自己主动置空,浏览器就会对其进行回收
  • 栈:当前上下文是否有内容被其他上下文变量所占用,如果有则无法释放(闭包)

8.6 闭包应用场景分析 - 循环添加事件

8.6.1 循环添加事件实现

<!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></head><body><button>按钮1</button><button>按钮2</button><button>按钮3</button><script src="./add_event_loop.js"></script></body>
</html>
// 索引打印出来全部为 3 ,存在问题
// 生成一个伪数组,每个成员都是button对象
var aButtons = document.querySelectorAll("button");
for (var i = 0; i < aButtons.length; i++) {aButtons[i].onclick = function () {console.log(`当前索引值为${i}`);};
}
// 当前索引值为3
// 当前索引值为3
// 当前索引值为3

解决方案1:闭包

var aButtons = document.querySelectorAll("button");
for (var i = 0; i < aButtons.length; i++) {(function (i) {aButtons[i].onclick = function () {console.log(`当前索引值为${i}`);};})(i);
}// 或var aButtons = document.querySelectorAll("button");
for (var i = 0; i < aButtons.length; i++) {aButtons[i].onclick = (function (i) {return function () {console.log(`当前索引值为${i}`);};})(i);
}

解决方案2:let(本质类似于闭包思想)

var aButtons = document.querySelectorAll("button");
for (let i = 0; i < aButtons.length; i++) {aButtons[i].onclick = function () {console.log(`当前索引值为${i}`);};
}

解决方案3:自定义属性

var aButtons = document.querySelectorAll("button");
for (let i = 0; i < aButtons.length; i++) {aButtons[i].myIndex = i;aButtons[i].onclick = function () {console.log(`当前索引值为${this.myIndex}`);};
}

解决方案4:事件委托

<button index="1">按钮1</button>
<button index="2">按钮2</button>
<button index="3">按钮3</button>
document.body.onclick = function (ev) {var target = ev.target;targetDom = target.tagName;if (targetDom === "BUTTON") {var index = target.getAttribute("index");console.log(`当前点击的是第 ${index} 个`);}
};

8.6.2 事件添加底层分析

分析如果在实际开发中,如果要用原生的方式实现循环添加事件,面对几种不同的实现方案,采用哪一种会好一些(事件委托是最好的方式)

九、代码优化

9.1 测试 JavaScript 性能

  • 本质上就是采集大量的执行样本进行数学统计和分析从而得出比对结果来证明什么样的脚本效率更高
  • 可以使用 JSBench (在线测试JS代码的网站)完成

9.2 变量局部化

  • 可以提高代码的执行效率(减少了数据访问时需要查找的路径)
  • 在数据的存储和读取上减少访问层级,提升代码执行速度
var i,str = "";
function packageDom() {for (var i = 0; i < 1000; i++) {str += i;}
}
packageDom();function packageDom() {let str = "";for (let i = 0; i < 1000; i++) {str += i;}
}
packageDom();

9.3 缓存数据

  • 对于需要多次使用的数据进行提前保存,方便后续进行使用
  • 同样也是为了减少访问层级的查询(作用域链查找变快)
<!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></head><body><div id="skip" class="skip"></div><script>var oBox = document.getElementById("skip");function hasClassName(ele, cls) {console.log(ele.className);return ele.className == cls;}console.log(hasClassName(oBox, "skip")); // true//优化后---------------------------------------------------------------------function hasClassName(ele, cls) {// 假设在当前的函数体当中需要对 className 的值进行多次使用// 那么可以将它提前缓存起来var clsName = ele.className;console.log(clsName);return clsName == cls;}console.log(hasClassName(oBox, "skip")); // true</script></body>
</html>

9.4 减少访问层级

// 减少访问层级
function Person() {this.name = "zce";this.age = 40;
}
let p1 = new Person();
console.log(p1.age);// -----------------------------------------------
// 未减少
function Person() {this.name = "zce";this.age = 40;this.getAge = function () {return this.age;};
}
let p1 = new Person();
console.log(p1.getAge());

9.5 防抖与节流

9.5.1 需求场景

在一些高频率事件触发的场景下,我们不希望对应的事件处理函数多次执行

  • 滚动事件
  • 输入的模糊匹配
  • 轮播图切换
  • 点击操作

浏览器默认情况下都会有自己的监听事件间隔(4~6ms),如果检测到多次事件的监听执行,那么就会造成不必要的资源浪费

防抖

  • 场景:界面上有一个按钮,我们可以连续多次点击
  • 防抖:对于这个高频的操作来说,我们只希望识别一次点击,可以认为是第一次或者最后一次

节流

  • 场景:界面上有一个按钮,我们可以连续多次点击
  • 节流:对于这个高频的操作来说,我们可以自己来设置频率,让本来会执行很多次的事件触发,按照我们定义的频率减少触发次数

9.5.2 防抖函数实现

<!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></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 = 1000;if (typeof wait === "boolean") {immediate = wait;wait = 1000;}if (typeof immediate !== "boolean") immediate = false;// 所谓的防抖效果我们想要实现的就是有一个“人”可以管理 handle 的执行次数/* 如果我们想要执行最后一次,那就意味着无论我们当前点击了多少次。前面的 N-1 次都无用 */let timer = null;return function proxy(...args) {let self = this;init = immediate && !timer;clearTimeout(timer);timer = setTimeout(() => {timer = null;!immediate ? handle.call(self, ...args) : null;}, wait);// 如果当前传递进来的是 true 就表示我们需要立即执行// 如果想要实现只在第一次执行,那么可以添加 timer 为 null 作为判断// 因为只要 timer 为 null,就意味着没有第二次~第 N+1 次点击init ? handle.call(self, ...args) : null;};}// 定义事件执行函数function btnClick(ev) {console.log("点击了", this, ev);}// 当我们执行了按钮点击之后就会执行返回的 proxyoBtn.onclick = myDebounce(btnClick, 1000, true); // true 表示执行第一次</script></body>
</html>

9.5.3 节流函数实现

<!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(() => {// 这个操作只是将系统的定时器清除,但 timer 中的值还在clearTimeout(timer); timer = null;handle.call(self, ...args);previous = new Date();}, interval);}};}// 定义滚动事件监听function scrollFn() {console.log("滚动了");}window.onscroll = myThrottle(scrollFn, 1000);</script></body>
</html>

9.6 减少判断层级

优化前

function doSomething(part, chapter) {const parts = ["ES2016", "工程化", "vue", "react", "node"];if (parts.includes(part)) {console.log("属于前端范畴");if (chapter > 5) {console.log("您需要提供 VIP身份");}} else {console.log("请确认模块信息");}
}
doSomething("ES2016", 6);

优化后

function doSomething(part, chapter) {const parts = ["ES2016", "工程化", "vue", "react", "node"];if (!part) {console.log("请确认模块信息");return;}if (!parts.includes(part)) return;console.log("属于前端范畴");if (chapter > 5) {console.log("您需要提供 VIP身份");}
}
doSomething("ES2016", 6);

9.7 减少循环体活动

优化前

var test = () => {var i;var arr = ["zce", 38, "张三"];for (i = 0; i < arr.length; i++) {console.log(arr[i]);}
};
test();

优化后

var test = () => {var i;var arr = ["zce", 38, "张三"];var len = arr.length;for (i = 0; i < len; i++) {console.log(arr[i]);}
};
test();


再次优化

var test = () => {var arr = ["zce", 38, "张三"];var len = arr.length;while (len--) {console.log(arr[len]);}
};
test();

9.8 字面量与构造式

优化前

let test = () => {let obj = new Object();obj.name = "zce";obj.age = 38;obj.slogan = "张三";return obj;
};
console.log(test());
var str = new String('张三')
console.log(str);

优化后

let test = () => {let obj = {name: "zce",age: 38,slogan: "张三",};return obj;
};
console.log(test());
var str = '张三'
console.log(str);


JavaScript 深度剖析 - JavaScript 性能优化相关推荐

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

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

  2. (转)Javascript的DOM操作 - 性能优化

    转载:https://my.oschina.net/blogshi/blog/198910 摘要: 想稍微系统的说说对于DOM的操作,把Javascript和jQuery常用操作DOM的内容归纳成思维 ...

  3. 深入云原生 AI:基于 Alluxio 数据缓存的大规模深度学习训练性能优化

    作者 | 车漾(阿里云高级技术专家).顾荣(南京大学 副研究员) 导读:Alluxio 项目诞生于 UC Berkeley AMP 实验室,自开源以来经过 7 年的不断开发迭代,支撑大数据处理场景的数 ...

  4. 阿里云原生实践:基于 Alluxio 数据缓存的大规模深度学习训练性能优化

    导读:Alluxio 项目诞生于 UC Berkeley AMP 实验室,自开源以来经过 7年的不断开发迭代,支撑大数据处理场景的数据统一管理和高效缓存功能日趋成熟.然而,随着云原生人工智能(Clou ...

  5. 工程之道,解读业界最佳的深度学习推理性能优化方案

    本文转载自旷视研究院 MegEngine「训练推理一体化」的独特范式,通过静态图优化保证模型精度与训练时一致,无缝导入推理侧,再借助工业验证的高效卷积优化技术,打造深度学习推理侧极致加速方案,实现当前 ...

  6. 深度学习推理性能优化,一个越来越重要的话题

    向AI转型的程序员都关注了这个号???????????? 机器学习AI算法工程   公众号:datayx 为什么我们开始关注和重视推理性能的优化. 天时 深度学习的上半场主题是自证, 数据科学家们设计 ...

  7. 【基本功】深入剖析Swift性能优化

    简介 2014年,苹果公司在WWDC上发布Swift这一新的编程语言.经过几年的发展,Swift已经成为iOS开发语言的"中流砥柱",Swift提供了非常灵活的高级别特性,例如协议 ...

  8. 深扒!从六个角度深度剖析Java性能调优,附带实战演练

    让你的Java程序更快.更稳定 程序的性能受代码质量的直接影响.那么该如何让代码在级别上提升系统性能呢? 其实性能提升永远没有捷径,需要 分析.优化.实验.监控 ,需要一点点积累和深入.随着你对项目和 ...

  9. js1:对象的学习,构造函数,继承构造函数【使用教材:JavaScript深度剖析第2版】...

    原文发布时间为:2008-11-08 -- 来源于本人的百度文章 [由搬家工具导入] <html> <head> <title>js</title> & ...

最新文章

  1. Android中实现Bitmap在自定义View中的放大与拖动
  2. 运营资源很少的时候,怎么运营自己的产品(完结)
  3. Struts2框架概述及运行流程
  4. 网页服务器和mysql服务器_实现Web服务器之间使用同一个MYSQL和相同的网页配置文件的方法...
  5. C# PagedList 真分页
  6. 在微型计算机中 存储容量为1kb 指的是,2016年计算机一级考试模板
  7. return 、break 和 continue的区别
  8. Spring-Jpa : @MappedSuperclass的作用
  9. selenium系列--测试脚本--将Excel文件用于测试(unittest数据驱动实战)
  10. [笔记]unity渲染相关各种方案总结
  11. 《小岛经济学》读书笔记摘录
  12. 什么是VMWare虚拟机
  13. 教你用python画动态爱心表白
  14. win7系统备份方法
  15. ie禁用java怎么办,您如何解决IE中禁用javascript的问题?
  16. Android应用内安装apk包
  17. python实现录屏录音小工具
  18. (js)switch
  19. CStdioFile 一些基本用法
  20. 击碎瓶颈《华为项目管理体系最佳实践 》青岛站

热门文章

  1. 央行:规范金融业开源应用与发展的指导意见
  2. 幻影pin_幻影CSS
  3. 企业级日志平台新秀!比 ELK 更轻量、高效
  4. 为什么Google Home将成为Amazon Echo最可怕的噩梦?
  5. JDK环境变量配置-win10
  6. 扩展坞网口不能使用,待机后无效设备
  7. 2021年西安二手房价格
  8. oj1904: 寄居蟹与海葵
  9. 6.3 收敛性与稳定性
  10. 抽象代数 04.07 Jordan-Holder定理