Part1-2-3 JavaScript 性能优化
Posted 沿着路走到底
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Part1-2-3 JavaScript 性能优化相关的知识,希望对你有一定的参考价值。
javascript 内存管理
申请内存空间
使用内存空间
释放内存空间
常见GC算法
引用计数
标记清除
标记整理
分代回收
引用计数算法
核心思想:设置引用数,判断当前引用数是否为0
引用计数器
引用关系改变时,修改引用数字
引用数字为0时立即回收
引用计数算法优点
- 发现垃圾时立即回收
根据当前引用数是否为0,来决定这个对象是不是垃圾,如果判定为垃圾,立即进行释放
- 最大限度减少程序暂停
应用程序在执行过程中,必然会对内存进行消耗,而内存空间是有上限的,当内存快被占满时,引用计数会找到引用数为0的对象,进行释放
引用计数算法缺点
- 无法回收循环引用的对象
上图代码中,当代码执行完后,虽然在全局中,已经找不到obj1和obj2,但是在函数内,由于相互引用,所以obj1和obj2的引用计数就不是为0,造成引用计数算法无法回收这2个对象的内存空间,造成了内存空间浪费。
- 时间开销大
因为当前的引用计数需要去维护一个数值的变化,所以在这种情况下,要时刻监控当前对象的引用数值是否需要修改,对象引用计数的修改就需要消耗时间,需要修改的对象越多,则需要的时间也就越多。
标记清除算法
核心思想:分标记和清除二个阶段完成
遍历所有对象找标记活动对象
遍历所有对象清除没有标记对象
回收相应的空间
- 遍历所有对象找到活动对象进行标记。
- 遍历所有对象找到没有标记的对象,进行清除,同时去除所有对象的标记,以便下一次正常的工作。
通过2次的遍历行为,把当前的垃圾空间进行回收,最终交给空闲列表进行维护。
标记清除算法优点
解决了引用计数无法回收循环引用对象的问题
标记清除算法缺点
会造成空间碎片化。
由于所回收的对象在内存地址上是不连续的,因此造成空闲区间也是不连续的。
标记整理算法原理
标记整理算法可以看做是标记清除算法的增强
标记阶段的操作和标记清除算法一致
清除阶段会先执行整理,移动对象位置,让地址产生连续
标记整理算法图示
会先把活动对象进行标记,然后进行整理,将活动对象进行移动,在地址上变成连续的位置,
然后将活动对象整理后以外的区域进行整体回收,
这样就使得回收后的空间是连续的
2
认识V8
V8是一款主流的 JavaScript 执行引擎, Chrome浏览器、NodeJs都在使用
V8采用即时编译,将源码翻译成当前可以直接执行的机器码,使得执行速度很快
V8内存设限,64位操作系统不超过1.5G, 32位操作系统不超过800M
V8垃圾回收策略
采用分代回收的思想
内存分为新生代、老生代
针对不同对象采用不同算法
V8垃圾回收策略图示
V8中常用GC算法
分代回收
空间复制
标记清除
标记整理
标记增量
V8如何回收新生代对象
V8内存分配
V8内存空间一分为二,左侧用于存储新生代对象
小空间用于存储新生代对象 (64位操作系统大小是 32M | 32位操作系统大小是 16M)
新生代指的是存活时间较短的对象,如函数内声明的对象
新生代对象回收实现
回收过程采用空间复制算法 + 标记整理
新生代内存区分为二个等大小空间
使用空间为 From, 空闲空间为 To
活动对象存储于 From 空间
标记整理后将活动对象拷贝至 To
From 与 To 交换空间完成释放
回收细节说明
拷贝过程中可能出现晋升,拷贝过程中如果某个变量所指引的空间在当前的老生代对象里也会出现,这时候就会出现晋升操作
晋升就是指将新生代对象移动至老生代进行存储
一轮 GC后 还存活的新生代需要晋升
To 空间的使用率超过 25%时,将活动对象移动至老生代进行存储
老生代对象说明
老生代对象存放在右侧老生代区域
内存大小限制:64位操作系统 1.4G , 32位操作系统 700M
老生代对象就是指存活时间较长的对象:如果全局对象,闭包内的对象
老生代对象回收实现
主要采用标记清除、标记整理、增量标记算法
首先使用标记清除完成垃圾空间的回收
采用标记整理进行空间优化:当有新生代对象往老生代存储区域进行移动的时候,而这时老生代存储空间不足以存放新生代对象时,这时候就会触发标记整理
采用增量标记进行效率优化
增量标记如何优化垃圾回收
当垃圾回收进行工作时,会阻塞 JavaScript 程序执行。
将我们一整段的垃圾回收操作,拆分成多个小步,组合着去完成当前整个垃圾回收。
实现垃圾回收和程序执行交替完成,这样所带来的时间消耗会更加合理一些。
细节对比
新生代区域垃圾回收使用空间换时间
老生代由于存储空间大,因此老生代区域垃圾回收不适合不适合复制算法
Performance 使用步骤
打开浏览器输入目标网址
进入开发人员工具面板,选择性能
开启录制功能,访问具体界面
执行用户行为,一段时间后停止录制
分析界面中记录的内存信息
内存问题的外在表现
页面出现延迟加载或经常性暂停:与GC存在着频繁地垃圾回收操作相关,之所以会存在频繁垃圾回收操作,说明是有一些代码瞬间让内存爆掉了,需要去进行定位。
页面持续性出现糟糕性能表现:说明存在着内存膨胀,当前界面为了达到最佳的使用速度,去申请了一定的内存空间,而这所需要的的内存空间大小远超过了当前设备所能提供的内存大小
页面的性能随时间延长越来越差: 说明存在着内存泄漏
监控内存的几种方式
浏览器任务管理器
Timeline 时序图记录
堆快照查找分离 DOM
判断是否存在频繁的垃圾回收
为什么确定频繁垃圾回收
GC 工作时应用程序是停止的
频繁且过长的 GC 会导致应用假死
用户使用中感知应用卡顿
确定频繁的垃圾回收
Timeline 中频繁的上升下降
任务管理器中数据频繁的增加减小
V8 引擎工作流程
浏览器环境
Scanner是一个扫描器,对纯文本的 JavaScript 代码进行词法分析,会把代码分析成不同的 tokens,tokens是语法上无法分割的最小单位了
代码: const username = 'alishi'
经过Scanner扫描后得到相应的 token
Parser是一个解析器,解析的过程就是一个语法分析的过程,将词法分析的结果 tokens 转换成抽象的语法树。
会在语法分析的过程中做语法校验,如果有语法错误,会抛出错误。
下图是 AST语法分析后的内容
PreParser 是预解析,之所以需要预解析,是因为代码中可能会有很多声明,后期并没有使用。
Parser之后会转成字节码,然后变成机器码,然后才能去执行。
所以这些没有使用的声明 进行 Parser(全量解析)就没有意义。
因此要减少函数嵌套,避免过多的解析
lgnition 是 V8 提供的一个解释器,将抽象语法树转为字节码。
TurboFan 是 V8 提供的编译器模块,把字节码转化为具体的汇编代码。然后开始代码执行。
JSBench 使用
代码优化
慎用全局变量
变量局部化
这样可以提高代码的执行效率(减少了数据访问时需要查找的路径)
缓存全局变量
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>缓存全局变量</title>
</head>
<body>
<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>1111</p>
<input type="button" value="btn" id="btn5">
<input type="button" value="btn" id="btn6">
<p>222</p>
<input type="button" value="btn" id="btn7">
<input type="button" value="btn" id="btn8">
<p>333</p>
<input type="button" value="btn" id="btn9">
<input type="button" value="btn" id="btn10">
<script>z
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 // 将 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')
}
</script>
</body>
</html>
通过原型新增方法
在原型对象上新增实例对象需要的方法
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()
避开闭包陷阱
function foo() {
var el = document.getElementById('btn')
el.onclick = function() {
console.log(el.id)
}
el = null
}
foo()
避免属性访问方法使用
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
减少判断层级
function doSomething(part, chapter) {
const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']
if (part) {
if (parts.includes(part)) {
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
if (chapter > 5) {
console.log('您需要提供vip身份')
}
}
For 循环优化
减少循环体活动
var arrList = []
arrList[10000] = 'icoder'
for (var i = 0; i < arrList.length; i++) {
console.log(arrList[i])
}
// 将 数组长度 储存起来,避免每次计算长度
for (var i = 0, len = arrList.length; i < len; i++) {
console.log(arrList[i])
}
选择最优的循环方法
var arrList = new Array(1, 2, 3, 4, 5)
// forEach 最快
arrList.forEach(function(item) {
console.log(item)
})
for (var i = arrList.length; i; i--) {
console.log(arrList[i])
}
for (var i in arrList) {
console.log(arrList[i])
}
字面量与构造式
选择字面量以节省内存空间
function test() {
let obj = new Object()
obj.name = 'zce'
obj.age = 38
obj.slogan = '我为前端而活'
return obj
}
function test() {
let obj {
name: 'zce'
age: 38
slogan: '我为前端而活'
}
return obj
}
var str = new String('zce说我为前端而活')
var str = 'zce说我为前端而活'
防抖函数
var oBtn = document.getElementById('btn')
// handler: 执行函数,
// wait:控制多少时间内执行一次,
// immediate: 控制执行第一次还是最后一次 false 执行最后一次
function myDebounce(handler, wait, immediate) {
// 参数类型判断
if (typeof handler !== 'function') throw new Error('handler must be an function')
if (typeof wait === 'undefined') wait = 300
if (typeof wait === 'boolean') {
immediate = wait
wait = 300
}
if (typeof wait !== 'boolean') immediate = false
let timer = null
return function proxy (...args) {
const self = this
const init = immediate && !timer
clearTimeout(timer)
timer = setTimeout(() => {
!immediate ? handler.call(self, ...args) : null
}, wait)
// 如果 immediate 传递进来的是 true, 表示需要立即执行
init ? handler.call(self, ...args) : null
}
}
// 定义执行函数
function btnClick(ev) {
console.log('点击了')
}
oBtn.onclick = myDebounce(btnClick, 200, true)
节流函数
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 = 0
return function Proxy(...args) {
const now = new Date() // 记录当前执行的时间
const self = this
const interval = wait - (now - previous)
if (interval <= 0) {
clearTimeout(timer)
timer = null
handle.call(self, ...args)
previous = new Date()
} else if (!timer) {
timer = setTimeout(() => {
clearTimeout(timer)
timer = null
handle.call(self, ...args)
previous = new Date()
, interval)
}
}
}
// 定义滚动监听事件
function scrollFn() {
console.log('滚动了')
}
window.onscroll = myThrottle(scrollFn, 500)
函数执行时做的事情
- 确定作用域链: <当前执行上下文,上级执行上下文>
- 确定 this
- 初始化 arguments 对象
- 形参赋值
- 变量提升
- 执行代码
闭包
闭包是一种机制。
保护当前上下文当中的变量与其他的上下文中变量互不干扰。
保存当前上下文中的数据(堆内存)被当前上下文以外的上下文中的变量所引用,这个数据就保存下来。
函数调用形成了一个全新的私有上下文,在函数调用之后当前上下文不被释放就是闭包。(临时不被释放)
循环添加事件 推荐 使用 事件委托,内存占用最少,不会像闭包占用内存,利于垃圾回收。
this
## 关于 this 的回顾
```javascript
function foo () {
console.log(this)
}
foo() //
window.foo() //
foo.call(1) //
```
```javascript
const obj1 = {
foo: function () {
console.log(this)
}
}
obj1.foo() //
const fn = obj1.foo
fn() //
```
```javascript
const obj2 = {
foo: function () {
function bar () {
console.log(this)
}
bar()
}
}
obj2.foo()
function person () {
}
person() // 函数调用
new person() // 构造函数调用
```
关于 this 的总结:
1. 沿着作用域向上找最近的一个 function(不是箭头函数),看这个 function 最终是怎样执行的;
2. **this 的指向取决于所属 function 的调用方式,而不是定义;**
```js
```
1. function 调用一般分为以下几种情况:
1. 作为函数调用,即:`foo()`
1. 指向全局对象(globalThis),注意严格模式问题,严格模式下是 undefined
2. 作为方法调用,即:`foo.bar()` / `foo.bar.baz()` / `foo['bar']()` / `foo[0]()`
1. 指向最终调用这个方法的对象
3. 作为构造函数调用,即:`new Foo()`
1. 指向一个新对象 `Foo {}`
4. 特殊调用,即:`foo.call()` / `foo.apply()` / `foo.bind()`
1. 参数指定成员
2. 找不到所属的 function,就是全局对象
3. 箭头函数中的 this 指向
```js
function fn () {
let arrFn = () => {
console.log(this)
}
arrFn()
}
const obj = {
name: 'zs',
fn: fn
}
obj.fn()
fn()
```
then:
```javascript
var length = 10
function fn () {
console.log(this.length)
}
const obj = {
length: 5,
method (fn) {
fn() //
arguments[0]() // 3 相当于 arguments.fn() 有3个参数,参数的长度是3
}
}
obj.method(fn, 1, 2)
```
严格模式下原本应该指向全局的 `this` 都会指向 `undefined`
ES 2020/2021 新特性
// 空值合并运算符
function foo (option) {
// 只有 size = null 或者 undefined
// 0 有时也是一个有意义的值,短路运算判定为非后会获取不到
// ?? 只判断 null 或 undefined
option.size = option.size ?? 100
const mode = option.mode || 'hash'
console.log(option)
}
foo({ size: 0 })
// 可选链运算符
const list = [
{
title: 'foo',
author: {
name: 'zs',
email: 'zs@qq.com'
}
},
{
title: 'bar'
}
]
list.forEach(item => {
console.log(item.author?.name)
})
```
## ES11(2020)
### **1. Nullish coalescing Operator(空值处理)**
表达式在 ?? 的左侧 运算符求值为undefined或null,返回其右侧。
```
let user = {
u1: 0,
u2: false,
u3: null,
u4: undefined
u5: '',
}
let u2 = user.u2 ?? '用户2' // false
let u3 = user.u3 ?? '用户3' // 用户3
let u4 = user.u4 ?? '用户4' // 用户4
let u5 = user.u5 ?? '用户5' // ''
```
### **2. Optional chaining(可选链)**
?. 用户检测不确定的中间节点
```
let user = {}
let u1 = user.childer.name // TypeError: Cannot read property 'name' of undefined
let u1 = user.childer?.name // undefined
```
### **3. Promise.allSettled**
> 返回一个在所有给定的promise已被决议或被拒绝后决议的promise,并带有一个对象数组,每个对象表示对应的promise结果
```
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => reject('我是失败的Promise_1'));
const promise4 = new Promise((resolve, reject) => reject('我是失败的Promise_2'));
const promiseList = [promise1,promise2,promise3, promise4]
Promise.allSettled(promiseList)
.then(values=>{
console.log(values)
});
```
![图片](assets/640.png)
### **4. import('a' + 'xxx.js')**
按需导入
### **5. 新基本数据类型BigInt**
> 任意精度的整数
### **6. globalThis**
* 浏览器:window
* worker:self
* node:global
## **ES12(2021)**
### **1. replaceAll**
> 返回一个全新的字符串,所有符合匹配规则的字符都将被替换掉
```
const str = 'hello world';
str.replaceAll('l', ''); // "heo word"
```
### **2. Promise.any**
> Promise.any() 接收一个Promise可迭代对象,只要其中的一个 promise 成功,就返回那个已经成功的 promise 。
如果可迭代对象中没有一个 promise 成功(即所有的 promises 都失败/拒绝),就返回一个失败的 promise
```
const promise1 = new Promise((resolve, reject) => reject('我是失败的Promise_1'));
const promise2 = new Promise((resolve, reject) => reject('我是失败的Promise_2'));
const promiseList = [promise1, promise2];
Promise.any(promiseList)
.then(values=>{
console.log(values);
})
.catch(e=>{
console.log(e);
});
```
![图片](assets/640-20210427143353092.png)
### **3. WeakRefs**
> 使用WeakRefs的Class类创建对对象的弱引用(对对象的弱引用是指当该对象应该被GC回收时不会阻止GC的回收行为)
### **4. 逻辑运算符和赋值表达式**
> 逻辑运算符和赋值表达式,新特性结合了逻辑运算符(&&,||,??)和赋值表达式而JavaScript已存在的 复合赋值运算符有:
```
a ||= b
//等价于
a = a || (a = b)
a &&= b
//等价于
a = a && (a = b)
a ??= b
//等价于
a = a ?? (a = b)
```
### **5. 数字分隔符**
> 数字分隔符,可以在数字之间创建可视化分隔符,通过_下划线来分割数字,使数字更具可读性
```
const money = 1_000_000_000;
//等价于
const money = 1000000000;
1_000_000_000 === 1000000000; // true
```
使用 TypeScript 的 Vue.js 项目差异
1)基本操作
1. 安装 @vue/cli 最新版本
2. 使用 @vue/cli 创建一个项目(不选 TypeScript)
3. 使用 @vue/cli 安装 TypeScript 插件
```bash
vue add typescript
1
以上是关于Part1-2-3 JavaScript 性能优化的主要内容,如果未能解决你的问题,请参考以下文章