javaScript 性能优化
javaScript 性能优化介绍
- 性能优化不可避免
- 提高运行效率,降低运行开销的行为都可以看做为性能优化。
- 在前端中性能优化是无处不在,包括在请求资源时的网络行为,数据传输,开发框架都可以进行优化。
本次学习为 javascript 语言本身的优化
内容概要 :
- 内存管理基本流程
- 垃圾回收与常见的 GC 算法
- V8 引擎的垃圾回收机制
- Performance 工具监控内存
- 代码优化实例
javascript 内存管理
- 高级编程语言中都带有 GC 机制。
1. 内存为什么需要管理
function fn () {
arrList = []
arrList[10000] = 'lg is a coder'
}
fn()
选择比较大的下标是为了申请一块比较大的内存,代码在运行fn时内存占用是逐渐层高而未回落, 说明这个函数存在内存泄漏,而在代码中内存泄漏的代码多了会形成意向不到的bug, 所以内存管理是很有必要的。
2. 什么是内存管理
内存 : 由可读写单元组成,表示一片可以操作空间。
管理: 人为的去操作一片空间的申请 使用和释放。
内存管理 : 开发者主动去申请空间 使用空间 和释放空间。
管理流程: 申请 -> 使用 -> 释放
javascript 中的内存管理主要依靠垃圾回收机制,因为 ECMAScript 中并没有提供 API 来主动操作管理流程。
3. 内存空间生命周期的演示
// 定义一个变量来申请内存空间
let obj = {}
// 使用内存空间
obj.name = "lg"
// 释放内存空间
obj.name = null
javascript 中的垃圾回收
javascript 中内存管理是自动的,创建变量会自动分配内存空间
垃圾标准:
- 对象不再被引用时此时可以看做是垃圾会被垃圾回收机制回收
- 对象不能从根上访问到时是垃圾
涉及定义可达对象和根
-
可达对象 : 可以访问到的对象就是可达对象可以通过引用或者当前作用域链,
可达的标准是判断是否可以从根上找到。 -
根 : javascript 中的根就可以理解为全局变量对象或者全局执行上下文。
垃圾回收时先找到垃圾然后通过 javascript 引擎进行释放和回收以及再分配。
代码解释 javascript 中的引用与可达
引用
let obj = { name: 'xiaoming' } // 这块空间是小明的空间 obj是根上可达对象
let alice = obj // 对象obj新增一次引用
obj = null // 虽然obj的值为null 但是小明的空间仍然是可达的, 因为alice仍然在引用
可达
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)
// 打印为
{
o1: { name: 'obj1', next: { name: 'obj2', prev: [Circular] } },
o2: { name: 'obj2', prev: { name: 'obj1', next: [Circular] } }
}
GC 算法
GC 定义于作用
- 定义:GC 即为垃圾回收机制的简写
- 作用:GC 可以找到内存中的垃圾 并释放和回收空间
GC 里的垃圾是什么?
- 程序中不在需要使用的对象(程序需求)
function fn() {
name = 'lg'
return `${name} is a coder`
}
fn()
- 程序中不能再访问到的对象
function fn() {
const name = 'lg'
return `${name} is a coder`
}
GC 算法是什么?
- GC 是一种机制,垃圾回收器完成具体的工作。工作内容就是查找垃圾,释放空间以及回收空间。
- 算法就是工作时查找和回收所遵循的规则。
常见的 GC 算法 引用计算 标记清除 标记整理 分代回收。
一. 引用计数算法
1. 引用计数算法的原理
- 核心思想 : 设置引用数 判断当前引用数是否为 0 来判断是否为垃圾对象。
- 引用计数器 : 用来统计对象被引用次数的一个数值。
引用关系改变时修改引用数值 有对该对象的引用时引用数值加 1 否则则减 1,知道引用数字为 0 , 引用数字为 0 时会被立刻回收。
// reference count
const user1 = {
age: '1'
}
const user2 = {
age: '2'
}
const user3 = {
age: '3'
}
const nameLsit = [user1.age, user2.age, user3.age] // 因为数组仍然引用这上面三个对象所以上面三个对象不会被当做垃圾进行回收
function fn() {
// num1 = 1
// num2 = 2
const num1 = 1
const num2 = 2 // 前面加了 const 函数 fn 调用了之后外部无法访问 内部两个变量 所以会直接回收
}
fn()
2. 引用计数算法优缺点
-
优点
- 发现垃圾时立即回收。
- 最大限度减少程序暂停 : 程序运行会消耗内存, 如果内存沾满程序会暂停而计数算法可以立即回收垃圾对象。
-
缺点
- 无法回收循环引用的对象。
- 时间消耗更大 数值修改需要时间 而且监听也需要时间。
- 循环引用的对象
function fn() {
const obj1 = {}
const obj2 = {}
// obj1 和 obj2 fn 执行结束后在根上已经找不到这两个对象了 , 但是也没有被回收因为下面代码仍对它们有引用而垃圾回收机制是没办法进行回收 obj1 和 obj2 的因为它们处在循环引用中。
obj1.name = obj2
obj2.name = obj1
return 'ok'
}
fn()
二. 标记清除算法
1. 标记清除算法实现原理
- 核心思想 : 分标记和清除两个阶段。
- 遍历所有对象标记活动对象(活动对象和可达对象一样)。
- 遍历所有对象清除没有标记对象以及把已经标记的清除标记。
- 回收相应的空间, 把空闲的空间放进空闲列表中方便下一次的分配空间。
2. 标记清除算法优缺点
-
优点
- 解决对象循环引用的回收操作
-
缺点
- 标记清除算法回收的空间易有地址不连续, 对后续申请空间并使用并不友好也容易造成内存浪费。
空间碎片化: 当前回收的垃圾对象在地址上时不连续的, 由不连续所以空闲空间会分散, 如果新申请的空间和碎片化空间匹配
自然好,如果不匹配就不适合使用了。
三. 标记整理算法
1. 标记整理算法实现原理
- 标记整理可以看到是标记清除的增强
- 标记阶段操作和标记清除一致
- 清除会先执行整理,移动对象,就是把所有的活动对象标记后整理进行移动,可以最大限度的对回收后的空闲空间进行利用以及优化标记清除算法的空间碎片化。
常见 GC 算法的总结
- 引用计数:通过引用计数器来维护每个对象的引用计数,通过判断引用计数是否为 0 来判断这个对象是否是垃圾对象。如果为垃圾对象就会立刻进行回收以及释放空间,为再一次分配空间做准备。
- 标记清除:遍历所有对象,对活动对象进行标记,然后对活动对象进行清除 存在问题是容易造成空间碎片化。
- 标记整理: 遍历所有对象,标记活动对象,对活动对象进行整理排序。进行清除 避免了空间随便化。
V8 引擎
1. 认识 V8 引擎
- V8 是一款主流的 javascript 执行引擎 chrome 以及 node 都在使用 V8 引擎
- V8 可以高效执行 javascript
- V8 采用即时编译 速度和快
- V8 内存是设限的 64 位系统 1.5G 32 位系统 800M
设限原因:
- V8 主要服务于网页的, 当前设置限制大小对网页应用来说足够了 。
- V8 的垃圾回收机制也决定了设置内存限制是合理的当 内存消耗达到 1.5G 假设使用增量标记的算法回收也只需要 50ms,如果使用非增量标记算法需要 1s。
2. V8 的垃圾回收策略
- 采用分代回收的思想 ,内存分为新生代和老生代 ,从而针对不同对象采用不同算法。
V8 常用的 GC 算法 :分代回收 空间赋值 标记清除 标记整理 标记增量
1. 新生代空间如何优化垃圾回收
- V8 内存分配为一分为二。
- 小空间用于存储新生代对象 (64 位操作系统 32M| 32 位操作系统 16M)。
- 新生代是指相对存活时间较短的对象 例如局部作用域内的变量 函数执行结束就会被回收而全局的变量则要等程序退出才会进行回收。
新生代对象回收实现
-
回收采用复制算法 + 标记整理算法。
-
新生代内存区分为两个等大的空间, 使用空间为 From 空闲空间为 To, 将活动对象存储在 From 空间中, 当 From 空间达到定饱和度的时候就会进行标记整理。
-
将标记整理后的活动对象拷贝至 To 空间, 然后开始回收,回收的体现就是 From 空间和 To 空间的交换完成释放。
-
回收细节说明:
-
拷贝过程中可能出现晋升 : 就是在拷贝的过程中发现一个对象 在老生代中也存在 ,就会出现一个晋升的操作,也就是把新生代的对象移动至老生代进行村存储。
-
触发晋升操作场景:
-
- 一轮 GC 还存活的新生代需要晋升。
-
- To 空间的使用率超过 25 %。
-
-
注: 如果 To 空间中的使用率超过 80 % 后 From 空间和 To 空间交换之后 新的活动对象可能就存不进当前的 From 对象中了。
2. 老生代空间如何优化垃圾回收
-
老生代对象说明
- 老生代对象放在右侧老生代区域,同样对内存大小由限制,64 位操作系统位 1.4G, 32 位操作系统为 700M。
- 老生代对象就是存活时间较长的对象 例如 闭包内的变量和全局变量。
老生对对象回收实现
- 主要采用标记清除,标记整理,增量标记算法。
- 首先使用标记清除完成垃圾空间的回收,回收后会产生一些空间碎片化的问题。
- 新生代的晋升会触发标记整理, 整理标记清除回收后空间碎片化的空间, 达到空间优化以存入新生代晋升的对象。
- 之后会采用增量标记进行效率优化。
老生代垃圾回收和新生代垃圾回收的细节对比
-
新生代区域垃圾会后使用空间交换时间, 新生代采用空间交替的方法,所以实时都会有空闲的空间,造成空间浪费
-
老生代区域垃圾回收不适合赋值算法, 老生代中的对象比较多,而且老生代空间比新生代要大的多,如果实时都有空闲的太过浪费
3. 标记增量如何优化垃圾回收
- 垃圾回收会阻塞 javascript 执行, 程序完成后停下来进行回收垃圾,而标记增量可以使垃圾回收和程序执行交替进行,而不必像之前那样程序执行不做垃圾回收,垃圾回收时不做程序执行,这样带来的时间消耗更为合理。
Performance 工具
为什么使用 Performance 工具 ?
GC 算法的目的就是为了实现内存精简的良性循环,良性循环的基石就是合适使用内存空间, 但是 javascript 并没有提供这系列的 API 来进行操作。我们想去判断内存使用是否合理,需要时刻关注内存使用的变化,而 Performance 这个工具就可以让我们可以时刻关注到内存使用的变化,以便于定位由内存使用有问题的代码块。
- Performance 使用步骤
- 打开浏览器输入目标地址
- 进入开发人员工具面板 选择性能
- 开启录制功能 访问具体界面
- 执行用户行为 一段时间后停止录制
- 分析界面中记录的内存信息
- chrome 浏览器
步骤
- 打开开发者工具 点击 Performance
- 点击圆点 开始一个新的录制
- 发起请求模拟用户操作后结束录制
- 勾选 Screenshots 和 memory 查看内存使用的线图分析内存使用问题
内存问题的体现
配合 Performance 定位问题
-
页面出现延迟加载或者经常性的暂停(网络环境正常且频繁操作 GC 的垃圾回收) :意味着代码中有些代码瞬间让内存爆掉了,触发了 GC 垃圾回收机制。
-
页面持续性出现糟糕的性能(底层出现内存膨胀)。
内存膨胀:当前界面为了达到最佳使用速度去申请内存空间 ,申请的空间大小超过了设备提供的大小。
- 页面性能随时间延长越来越差 (代码出现内存泄漏)。
内存监控的方式
常见内存监控的方式
- 浏览器任务管理器 可以以数值来体现当前页面运行时内存的变化
- Timeline 时序图记录 可以把内存的走势以时间来记录
- 浏览器中有堆快照查找分离 DOM 分离 DOM 其实就是一种内存泄漏
界定内存问题的标准
-
内存泄漏: 内存使用持续升高 可以拿到内存走势图可以从走势图中看到
-
内存膨胀: 在多数设备上都存在性能问题 有可能是当前设备不具备申请空间的条件 应该多更换其他设备进行测试 如果多设备上都存在性能问题,则为内存膨胀
-
频繁垃圾回收: 通过内存变化图进行分析 界面无法感知
-
判断是否存在频繁的垃圾回收
一. 任务管理器监控内存
- shift + esc 打开任务管理器
<button id="btn">Add</button>
<script>
const oBtn = document.getElementById('btn')
oBtn.onclick = function () {
let arrList = new Array(1000000)
}
</script>
- 页面介绍 : 我们比较关注的是内存和 javascript 内存 , 内存是指本来 dom 节点的内存 ,如果内存的值不算增加表示一直在创建新的 dom 节点, javascript 内存指的是 js 的堆我们要看的是小括号中的值。小括号中的值是当前页面所有可达对象正在使用的内存值 , 如果这个值一直增大表示当前页面的内存是有问题的, 但是具体问题出现在哪里,任务管理器是不能帮助我们看到的 。
二. TimeLine 记录内存
-
作用 :具体定位内存出现问题的时间和代码
-
模拟场景:创建大量 dom 节点模拟内存消耗和数组遍历存入大量数据
<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>
- 根据 TimeLine 的图标分析可以看到内存何时增加何时回落 , 以及内存使用是否正常可以定位到有问题的代码.
三. 堆快照查找分离 DOM
什么是分离 DOM
- 界面元素存活在 DOM 树上
- 垃圾对象时的 DOM 节点
- 分离状态的 DOM 节点
找到 js 堆产生照片的留存,对分离 dom 的查找行为。
<button id="btn">Add</button>
<script>
var temEle
function fn() {
var ul = document.createElement('ul')
for (var i = 0; i < 10; i++) {
var li = document.createElement('li')
ul.appendChild(li)
}
temEle = ul
}
document.getElementById('btn').addEventListener('cick', fn)
</script>
操作: 打开浏览器中的 memory 点击 take snapshot 拍摄当前的堆快照, 在新增 dom 后拍摄新增后的堆快照, 新增后的堆快照可以通过搜索 deta 确认是否存在分离 dom
是否存在频繁的 GC 算法
如何确定频繁垃圾回收?
GC 工作时应用程序会停止,频繁且过长的 GC 会导致应用假死,用户使用中感知应用卡顿。
- TimeLine 中频繁的上升下降
- 任务管理器中数据频繁的增加减小
Performance 使用总结
- Performance 的使用流程
- 内存问题的相关分析,内存泄漏,内存膨胀,频繁的 GC 操作。
- Performance 时序图监控内存变化,监控内存是否是一直上升没有下落。
- 任务管理器监控内存变化,从 javascript 的数值来推算内存变化是否正常。
- 堆快照查找分离 DOM,分离 DOM 必然存在内存泄漏 ,通过堆快照来定位问题代码。
代码优化
1. 如何精准测试 javascript 性能
- 本质上就是采集大量执行样本进行数学统计和分析,而对于写代码的我们可能并不擅长做大量的数据统计和分析,我们可以使用基于 Benchmark.js 的https://jsperf.com/完成,它是一个在线的js脚本的性能测试网站。
jsperf 的使用流程
- 使用 GitHub 账号登录
- 填写个人信息(非必须)
- 填写详细的测试用例信息( title slug)
- 填写准备代码(DOM 操作时经常使用)
- 填写必要有 setUp 与 teardown 代码 setUp 理解为操作前准备工作 ,teardown 就是用完后的销毁动作
- 填写测试代码片段
2. 慎用全局变量
程序执行过程中如果有些数据需要存储尽量使用局部作用域中变量进行存储。
为什么要慎用?
- 全局变量定义在去哪聚执行上下文, 是所有作用域链的顶端。
- 全局执行上下文一直存在于上下文执行栈,直到程序退出。
- 如果某个绝不作用域出现了同名变量则会遮蔽或污染全局。
var i, str = ''
for (i = 0; i < 1000; i++) {
str += i
}
for (let i = 0; i < 1000; i++) {
let str = ''
str += i
}
使用 jsperf 测试哪一段代码的执行速度更高
结果局部变量的代码执行速度远高于全局变量
3. 缓存全局变量
缓存全局变量: 将使用中无法避免的全局变量缓存到局部。
使用两种方法写入代码使用 jsperf 去测试两端代码的运行速度。
<input type="button" value="btn" id='btn1'>
<input type="button" value="btn" id='btn2'>
<input type="button" value="btn" id='btn3'>
<input type="button" value="btn" id='btn4'>
<p>11111</p>
<input type="button" value="btn" id='btn5'>
<input type="button" value="btn" id='btn6'>
<input type="button" value="btn" id='btn7'>
<p>22222</p>
<input type="button" value="btn" id='btn8'>
<input type="button" value="btn" id='btn9'>
<p>33333</p>
<input type="button" value="btn" id='btn10'>
<script>
// 代码片段1
function getBtn1() {
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')
}
// 代码片段2
function getBtn2() {
let obj = ducument
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')
}
</script>
结果是使用局部缓存全局变量的运行速度更快。
4. 通过原型对象添加附加方法
javascript 中有构造函数,原型对象,实例对象三种概念。
通过原型对象添加附加方法:如果某实例对象需要频繁使用一个方法可以在原型对象上新增实例对象需要的方法
方法挂载到原型上和普通构造函数上的运行速度比较
var fn1 = function () {
this.too = function () {
console.log('111');
}
}
let f1 = new fn1()
var fn2 = function () {
fn2.prototype.foo = function () {
console.log('1111');
}
}
5. 避开闭包陷阱
闭包的特点:外部具有指向内部的引用, 在外部作用域访问内部作用域的数据 。
function foo() {
var name = 'zs'
function fn() {
console.log(name);
}
return fn
}
var a = foo()
a()
关于闭包
- 闭包是一种强大的语法
- 闭包使用不当容易造成内存泄漏
- 不要为了闭包而闭包
<button id="btn">add</button>
<script>
function foo() {
var el = document.getElementById('btn')
el.onclick = function () {
console.log(el.id);
}
}
foo()
// 优化闭包陷阱
function foo() {
var el = document.getElementById('btn') // 因为本身button就存在相当于被引用一次 而el就是引用的第二次
el.onclick = function () {
console.log(el.id);
}
el = null // 即可以回收el被分配的内存了
}
foo()
</script>
6. 避免属性访问方法使用
js 不需属性的访问方法, 所有属性都是外部可见的
使用属性访问方法只会增加一层重定义, 没有访问的控制力。
属性访问方法的运行速度并不入直接访问属性来的快
7. For 循环优化
for 循环中优化为把数组的 length 用变量声明使用 jsperf 来看下两个执行速度.
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<p class="btn">add</p>
<script>
var aBtns = document.getElementsByClassName('btn')
for (var index = 0; index < aBtns.length; index++) {
console.log(index);
}
for (var index = 0, len = aBtns.length; index < len; index++) {
console.log(index);
}
</script>
结果存储 length 速度要快于普通的 for 循环。
8. 采用最优的循环方式
如何判断最优循环方式:大量数据遍历,遍历的方法有很多.
可以通过一段逻辑相同的代码使用不同的遍历方法来看下哪个循环方式是最优的
var arrList = new Array([1,2,3,4,5,6,7])
arrList.forEach((item) => {
console.log(item);
})
for (var index = arrList.length; index; index--) {
console.log(arrList[index]);
}
for (var index in arrList) {
console.log(arrList[index]);
}
forEach for for…in 进行对比 foreach 性能最高 其次是 for 再然后是 for in。
9. 节点添加优化
操作 dom 是非常耗性能的,这个过程必然会有回流和重绘
<div id='box1'></div>
<script>
// 1. 定义文档碎片容器
// 不优化
for (let index = 0; index < 10; index++) {
var oP = document.createElement('p')
oP.innerHTML = index
document.body.appendChild(oP)
}
// 优化
// 定义文档碎片容器
const fragEle = document.createDocumentFragment()
for (let index = 0; index < 10; index++) {
var oP = document.createElement('p')
oP.innerHTML = index
fragEle.appendChild(oP)
}
document.body.appendChild(fragEle)
// 2. 克隆优化节点操作
// 不优化
for (let index = 0; index < 3; index++) {
var oP = document.createElement('p')
oP.innerHTML = index
document.body.appendChild(oP)
}
// 克隆优化节点
var oldP = document.getElementById('box1')
for (let index = 0; index < 3; index++) {
var newP = oldP.cloneNode(false)
newP.innerHTML = index
document.body.appendChild(newP)
}
</script>
10. 直接量替换 Object 操作
// 第一种方式直接量
var a = [1, 2, 3]
// 第二种new创建数组根据下标写入
var a1 = new Array(3)
a1[0] = 1
a1[1] = 2
a1[2] = 3
直接量的速度或者说执行效率要优先。
11.JSBench使用
- JSBench : 在线测试js代码执行效率的网站 , 网站地址https://jsbench.me/。
为什么学习JSBench使用?
- jsperf目前已经停止维护
- 有些关于js性能的内容需要分享
JSBench页面介绍:
1. SetUp是指比较代码前准备工作,比如有html和js 把代码放进相应的SetUpHTML和SetUpJS当中
2. TestCase里面放要比较的js代码 enter test case name 放标题
3. TeardownJS中放入类似共同调用的函数方法之类的公共的比较js代码后续处理工作的js
4. Output(DOM)里面进行dom操作一般是不需要使用的, 下面是有RUN按钮用来开始执行比较的js代码。
注意点:
- 建议使用JSBench时只打开一个标签,打开其他标签会抢占资源可能会影响代码执行速度
- 执行代码测试的时候建议页面停留在当前,不要切换因为浏览器是有挂起功能的,也可能会影响比较执行速度。
- 尽可能多执行几次脚本去多数结果为准,一次执行可能会受到各种情况影响并不一定准确
- 一段代码的执行速度快并不代表代码的健壮性就好,不要太重视执行速度。
12. 堆栈中代码执行流程
let a = 10
function foo(b) {
let a = 2
function baz(c) {
console.log(a + b + c); // 输出7
}
return baz
}
let fn = foo(2)
fn(3)
上段代码执行过程:
- 开始执行这段代码会先创建一个ECStack 执行环境栈
- 初始化全局执行上下文就是在栈中划分一块内存
VO:
存储 a = 10
foo = AB1[[scope]] VO
fn = AB2
fn(3)
- foo函数需要再堆中划分一块地址为AB1
foo = AB1 [[scope]] VO
AB1中:
function foo (b) { ... }
name: foo
length:1
- foo中需要划分一个EC(foo)
this = window <
foo.ao, vo >
AO:
Arguments: [0, 2]
b = 2
a = 2
- baz函数需要在队中划分一块内存
baz = AB2 [[scope]] foo.AO
function baz(c) { ... }
name: baz
length:1
- baz中的代码需要划分一个EC
this = window
< baz.ao, foo.ao, vo >
AO:
/ Arguments: [0: 3]
// c = 3
console.log(a + b + c)
3+2+2 ===> 7
13. 减少判断层级
表现为使用return无效值来优化
function doSomething(part, chapter) {
const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']
if (part) {
if (parts.includes(part)) {
console.log('属于当前课程');
if (chapter > 5) {
console.log('您需要提供VIP身份');
}
}
} else {
console.log('请确认模块信息');
}
}
// 减少层级优化
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)
14. 减少作用域链查找层级
关注为什么减少层级以及减少层级怎么达到优化代码的 ?
// 关注内存的情况下 这种情况会更佳
var name = 'zce'
function foo () {
name = 'zce666' // 此时name是在修改了全局name的值
function baz() {
var age = 38
console.log(age);
console.log(name);
}
baz()
}
foo()
// 优化后的
var name = 'zce'
function foo () {
var name = 'zce666' // 新增了var之后等于在foo函数中新增了name变量 在执行console.log(name);时找baz函数找不到就向上层找 找到了,直接打印, 同时不影响全局的值。
function baz() {
var age = 38
console.log(age);
console.log(name);
}
baz()
}
foo()
- 从执行速度来看优化后的比较快
- 从内存占用情况下来看 第一种情况更优
- 从垃圾回收的情况看 第二种优化后的foo函数没有其他占用, 执行过后就会被回收了.从内存占用情况下来看。
从不同的角度来说各有各的优势在自己的项目中需要取舍
函数执行中会产生一个执行上下文 多次调用同一个函数会创造多个执行上下文,在执行函数过程中会先在自己的执行上下文中去找需要的变量, 如果没有找到则找上一级。
15. 减少数据读取次数
关注为什么要去减少数据读取次数,以及减少数据读取次数后对性能的影响?
-
减少数据读取次数即为尽可能减少对象成员的查找次数以及嵌套层级和嵌套深度。
-
方法就是提前缓存对象数据
-
需求: 进入一般都有一个广告的欢迎页页面, 上面有跳过按钮, 判断当前是否有跳过按钮, 如果没有进行其他操作
<div id="skip" class="skip"></div>
<script>
var oBox = document.getElementById('skip')
function hasEle(ele, cls) {
return ele.className == cls
}
console.log(hasEle(oBox, 'skip'));
// 优化后
var oBox = document.getElementById('skip')
function hasEle(ele, cls) {
var clsName = ele.className
return clsName == cls
}
console.log(hasEle(oBox, 'skip'));
结果是提前缓存数据变量运行的速度确实是快于普通代码, 但是这个结果是相对的 , 内存角度来说提前缓存变量需要一个中间变量来存储 ,虽然函数执行结束整个变量会被回收但是如果这个变量数据量庞大的话也是会占用很多内存, 具体要不要使用还是要看具体的场景。
16. 字面量与构造式
- 字面量去代替构造式也是分场景的
- 日期数组类型的数据 字面量和构造式得到的都是引用类型的, 测试两种方法的执行速度。
let test = () => {
let obj = new Object()
obj.name = "zce"
obj.age = 38
obj.slogan = "世上无难事"
return obj
}
console.log(obj);
// 简单来说就是把创建对象放进函数里面, 使用new的方法来创建一个对象
let test = () => {
let obj = {
name: 'zce',
age: 38,
slogan: '世上无难事'
}
return obj
}
console.log(obj);
JSBench去测试结果是字面量会优于构造式
堆栈分析:
-
调用函数 做的事情更多一些, 需要再堆中创建一块空间调用时使用堆中空间的数据
-
字面量就是直接分配了一个空间存储了对象中的属性
-
普通数据类型的比较
var str1 = '世界太大想去看看'
var str2 = new String('世界太大想去看看')
console.log(str1);
console.log(str2);
JSBench去测试结果是字面量会优于构造式
引用类型中字面量和构造式相差不太多,但是基础数据类型建议使用字面量, 当然如果还需要使用原型上的一些属性还是建议使用构造函数方式。
17. 减少循环体活动
主要关注循环功能,循环次数相同的情况下, 内部执行的代码越多执行速度就越慢,反之则速度快
方法:每次循环都要使用的数据都抽离到循环体的外面类似提前缓存
var test = () => {
var index
var arr = ['zce', 38, '世界那么大']
for (index = 0; index < arr.length; index++) {
// console.log(arr[index]);
}
}
test()
优化后
var test = () => {
var index
var arr = ['zce', 38, '世界那么大']
var len = arr.length
for (index = 0; index < len; index++) {
// console.log(arr[index]);
}
}
test()
优化后的速度更优
while优化
var test = () => {
var arr = ['zce', 38, '世界那么大']
var len = arr.length
while (len--) {
console.log(arr[len]); // 顺序会反过来
}
}
test()
两种优化进行比较结果while的执行速度更优。
两方面分析
- 代码量更少执行速度更优
- 从后往前小于0就不再执行更快
思路就是当循环体中有数据不变的变量拿到外面不必每次循环都去获取或者操作,从后往前比从前往后会更优一些。
18. 减少声明以及与语句数
分别减少声明以及语句数两种例子
<div id="box" style="width:100px;height:100px"></div>
<script>
// 减少声明
// 未优化
var oBox = document.getElementById('box')
var test = (ele) => {
let w = oBox.offsetWidth
let h = oBox.offsetHeight
return w * h
}
// 优化
var test = (ele) => {
return ele.offsetWidth * ele.offsetHeight
}
console.log(test(oBox))
// 减少语句数
// 未优化
var test = () => {
var name = "zce"
var age = 38
var slogan = "世界很大"
return name + age + slogan
}
// 优化后
var test = () => {
var name = "zce",
age = 38,
slogan = "世界很大"
return name + age + slogan
}
console.log(test());
两种相比较优化后的执行速度更快, 但是第一种的语义化更强, 更便于维护。
</script>
对比结果为优化后执行速度更快。
19 . 惰性函数
惰性函数被看为是高阶的语法 反而执行速度没有更快
var btn = document.getElementById('btn')
function foo() {
console.log(this); // <button id="btn">点击</button>
}
function addEvent(obj, type, fn) {
if (obj.addEventLisener) {
obj.addEventLisener(type, fn, false)
} else if (obj.attachEvent) {
obj.attachEvent('on' + type, fn)
} else {
obj['on' + type] = fn
}
}
// 如果调用次数多的情况下每次进来都要做很多的判断,每次判断条件很多对性能有影响
// 优化后
function addEvent(obj, type, fn) {
if (obj.addEventLisener) {
addEvent = obj.addEventLisener(type, fn, false)
} else if (obj.attachEvent) {
addEvent = obj.attachEvent('on' + type, fn)
} else {
addEvent = obj['on' + type] = fn
}
return addEvent
}
addEvent(btn, 'click', foo)
// 打印结果相同
// 但是如果只调用一次 优化前速度更快
// addEvent(btn, 'click', foo)
// addEvent(btn, 'click', foo)
// addEvent(btn, 'click', foo)
20.采用事件委托
事件委托的本质就是利用js的冒泡机制把本来需要给子元素注册的事件委托给父元素来完成事件监听,
优点:
- 减少内存占用 减少事件注册
考点一般就是给一堆li不考虑内存占用给每个li都绑定事件达到性能最优
<ul id="ul">
<li>ZCE</li>
<li>28</li>
<li>世界很大想去看看</li>
</ul>
<script>
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')
ul.addEventListener('click', showTxt, true)
function showTxt(ev) {
var obj = ev.target
if (obj.nodeName.toLowerCase() == 'li') console.log(obj.innerHTML);
}
// 元素列表很大使用原始的方法去注册事件会很耗性能还是事件委托执行较优
</script>