一、V8引擎的工作流程
首先通过一张图整体理解一下:
二、堆栈处理
1、基本的堆栈处理
2、引用类型堆栈处理
这里插入一道练习题检测是否理解:
var a, b, c, d;
a = b = c = d = {a: 1};
a.x = a = b.y = b = c.z = c = {};
console.log(a, b, c, d);
console.log(a===b, b===c, c===d, c===d.x, d.x===d.y, d.y===d.z);
解析:
- 声明a、b、c、d,均为undefined。
- a、b、c、d被赋值,均指向对象Object_1,现在为{a:1}。
- 对象Object_1被声明属性x,现在为{a:1, x:undefined},a.x即为Object_1.x。
- 对象Object_1被声明属性y,现在为{a:1, x:undefined, y:undefined},b.y即为Object_1.y。
- 对象Object_1被声明属性z,现在为{a:1, x:undefined, y:undefined, z:undefined},c.z即为Object_1.z。
- c被赋值,指向对象Object_2,现在为{}。
- c.z即Object_1.z,被赋值,指向对象Object_2。
- b被赋值,指向对象Object_2。
- b.y即Object_1.y,被赋值,指向对象Object_2。
- a被赋值,指向对象Object_2。
- a.x即Object_1.x,被赋值,指向对象Object_2。
- 此时a、b、c、d.x、d.y、d.z均指向对象Object_2,现在为{}。d指向Object_1,现在为{a:1, x:{}, y:{}, z:{}}。
3、函数堆栈处理
4、闭包堆栈处理
5、闭包与垃圾回收
三、性能优化场景
1、循环添加事件
场景需求:给多个button元素添加点击事件 基础代码:
var abtns = document.querySelectorAll('button')
for (var i = 0; i < abtns.length; i++) {
abtns[i].onclick = function () {
console.log(`当前的索引值${i}`)
}
}
// 最后点击button后,输出i的值都是3,期望是当前点击button索引的值
那为什么每次点击button,都会输出3呢?
改造一:使用立即执行函数的方式
var abtns = document.querySelectorAll('button')
for (var i = 0; i < abtns.length; i++) {
(function (i) {
abtns[i].onclick = function () {
console.log(`当前的索引值${i}`)
}
})(i)
}
图解代码的堆栈执行情况:
这种方式存在缺陷:图中被虚线框住的部分匿名函数创建、执行产生了闭包,无法出栈释放,因此消耗了更多的空间
为了避免内存的泄露,当在button不再被点击时,需要手动将abtns置空,这个GC就会释放闭包中堆栈占用内存
那如何才能尽量少开辟新的内存空间,也避免内存无法自动释放的问题呢?
改造二:使用自定义属性的方式
var abtns = document.querySelectorAll('button')
for (var i = 0; i < abtns.length; i++) {
abtns[i].myIndex = i
abtns[i].onclick = function () {
console.log(`当前的索引值${this.myIndex}`)
}
}
图解代码的堆栈执行情况:
改造三:采用事件委托的方式
document.body.onclick = function (ev) {
var target = ev.target,
targetDom = target.tagName
if (targetDom === "BUTTON") {
var index = target.getAttribute('index')
console.log(`当前点击的时第${index}个`)
}
}
图解代码的堆栈执行情况:
2、JSBench对JS性能测试
JSBench网址
模块介绍:
带Setup的都是填写一些前置初始化的代码。
Test case是添加测试用例,我们需要比对的JS代码填写在这个版块。
Teardown和Setup是相对的,可以理解为Teardown是做收尾的工作。好比链接完数据库之后,操作完数据,把连接给释放掉。这部分都是一样的,那我们可以不写在测试用例里面,而是把它抽离出来,写到Teardown中去。
使用介绍:
我们在Test Case中红框地方填写我们需要测试的代码,然后点击RUN之后,蓝框地方会输出结果。因为单位是ops/s也就是每秒钟的操作数,所以前面那个数值越大越好。
使用建议:
- 使用性能测试的时候,建议浏览器只打开一个标签页,因为开启过多标签页会抢占资源,测试结果不那么准确。
- 还有就是在运行的时候尽量保持在这个页面上,不要什么最小化页面去做别的事情,因为有可能会被我们的系统挂起,那么测试的结果不一定准确。
- 不能执行完一遍之后得到的结论就觉得是最终的答案,应该多执行几次取出现几率最高的结果。
- 不应该纠结于代码的执行时间,对于性能测试而言,关注的并不是只有时间。
3、变量局部化
建议将变量局部化(全局-->局部),提高代码的执行效率,减少了数据访问时需要查找的路径。
// 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()
4、缓存数据
对于需要多次使用的数据进行提前保存,后续进行使用
var oBox = document.getElementById('skip')
// function hasClassName(ele, cls) {
// // 假设在当前的函数体中需要对className的值进行多次使用,那么我们就可以将它提前缓存起来
// console.log(ele.className)
// return ele.className == cls
// }
function hasClassName(ele, cls) {
var clsName = ele.className
console.log(clsName)
return clsName == cls
}
console.log(hasClassName(oBox, 'skip'))
总结:
- 减少声明和语句数
- 缓存数据(作用域链查找变快)
5、减少访问层级
在访问对象属性时,为了减少访问的层级,可以将方法或变量缓存起来,或者将对象扁平化处理后再访问。
6、防抖与节流
为什么需要防抖与节流?
在一些高频率事件触发的场景下,我们不希望对应的事件处理函数多次执行。
场景:
- 滚动事件
- 输入的模糊匹配
- 轮播图切换
- 点击操作
- ......
根源: 浏览器默认情况下都会有自己的监听事件间隔(4~6ms),如果检测到多次事件的监听执行,那么就会造成不必要的资源浪费。
前置场景: 界面上有一个按钮,我们可以连续多次点击
防抖: 对于高频操作,我们只希望识别一次点击,可以人为设置是第一次或者最后一次。
<button id="btn">点击</button>
var oBtn = document.getElementById('btn')
// handle 最终需要执行的事件监听
// wait 事件触发之后多久开始执行
// imediate 控制执行第一次还是最后一次, false 执行最后一次
function myDebounce(handle, wait, immediate) {
// 1、参数类型判断以及默认值处理
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,
init = immediate && !timer
// 当前点击清除上一次的定时器,疯狂点击间隔小于waits时,只有最后一次执行
clearTimeout(timer)
timer = setTimeout(() => {
// 每次定时器回调函数执行,置空timer,为下次有效点击做准备
timer = null
!immediate ? handle.call(self, ...args) : null
}, wait)
// 如果当前传递进来的immediate是true,则立即执行handle
init ? handle.call(self, ...args) : null
}
}
// 定义事件的执行函数
function btnClick(ev) {
console.log('点击了', this, ev)
}
oBtn.onclick = myDebounce(btnClick, 200, true)
// 考虑以前的点击事件中含有当前点击对象event以及this
// oBtn.onclick = myDebounce(event)
节流: 对于高频操作,我们可以自己设置频率,让本来会执行很多次的事件触发,按着我们定义的频率减少触发的次数。
// 节流:指的是在自定义的一段时间内让事件进行触发
function myThrottle(handle, wait) {
if (typeof handle !== 'function') throw new Error('handle must be function')
if (typeof wait === undefined) wait = 400
let previous = 0 // 定义变量记录上一次执行的时刻时间点
let timer = null // 用它来管理时间
return function proxy(...args) {
let self = this
let now = new Date() // 定义变量记录当前次执行的时刻时间点
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 = scrollFn
window.onscroll = myThrottle(scrollFn, 600)
图解以上代码的实现逻辑:
7、减少判断层级
场景需求:当不同的用户登录课程系统后,访问某个章节的某节课,判断权限逻辑。
多层判断层级的实现:
function doSomeThing (part, chap) {
const parts = ['ES6', '工程化', 'VUE', 'React', 'Node']
if (part) {
if (parts.includes(part)) {
console.log('属于当前章节')
if (chap > 5) {
console.log('您需要VIP 身份')
}
}
} else {
console.log('请确认模块信息')
}
}
doSomeThing('ES6', 6)
减少判断层级的实现
function doSomeThing (part, chap) {
const parts = ['ES6', '工程化', 'VUE', 'React', 'Node']
if (!part) {
console.log('请确认模块信息')
return
}
if (!parts.includes(part)) return
console.log('属于当前章节')
if (chap > 5) {
console.log('您需要VIP 身份')
}
}
doSomeThing('ES6', 6)
对比性能提升:
总结编程思想:
- 当我们在进行多层区间判断的时候(
if...else...),可以根据需求将多层嵌套扁平化处理,对异常情况优先处理并返回,正常流程滞后处理。 - 当我们在进行多个可枚举判断条件时,尽量使用
switch语句,因为if...else...主要用于区间判断。 - 易于维护的代码并不代表性能最优,需要我们在可维护和高性能之间取舍和平衡。
8、减少循环体活动
思路:循环体中是我们需要多次重复执行的代码,一般循环体越多,执行效率越低,因此我们要对循环体进行优化。
// var test = () => { // 一般写法
// var i
// var arr = ['aaa', 'bbb', 123]
// for (i = 0; i < arr.length; i++) {
// console.log(arr[i])
// }
// }
// var test = () => { // 缓存循环体不变量
// var i
// var arr = ['aaa', 'bbb', 123]
// var len = arr.length // 缓存循环体中的不变量
// for (i = 0; i < len; i++) {
// console.log(arr[i])
// }
// }
var test = () => { // // 缓存循环体不变量,同时减少条件判断
var arr = ['aaa', 'bbb', 123]
var len = arr.length // 缓存循环体中的不变量
while (len--) { // 采用while条件自减方式,减少条件判断
console.log(arr[len])
}
}
test()
对比性能(执行时间)提升:
总结编程思想:
- 对循环体中的不变量,避免通过查找的方式取值,可以提前进行缓存。
- 对于循环体输出内容顺序没要求时,可以采用while循环判断条件后自减的方式,减少条件判断次数。
9、字面量与构造式
思考:不同的变量声明方式在性能方面有什么差别呢? 引用类型的字面量与构造式的定义:
// var test = () => {
// let obj = new Object() // 调用了Object函数
// obj.name = 'aaa'
// obj.age = 20
// obj.slogan = 'xxxxxxxxxxxx'
// return obj
// }
var test = () => {
let obj = {
name: 'aaa',
age: 20,
slogan: 'xxxxxxxxxxxx'
}
return obj
}
console.log(test())
对比性能(执行时间)提升:
分析:两者有一些差异,第一种方式因为执行new Object()调用了Object函数导致时间多了。
基本类型的字面量与构造式的定义:
var str1 = 'xxxxxxxxxxxx'
var str2 = new String('xxxxxxxxxxxx')
console.log(str1)
console.log(str2)
对比性能(执行时间)提升:
分析:基本类型字面量定义相比于构造式定义,执行时间有极大的提升。原因是new String() 会开辟堆内存,将值与原型对象存储起来。字面量的方式str1也是String对象的一个实例,按照原型链也可以找到scice等方法