1 内存管理
内存由可读写单元组成,表示一片可操作空间
人为的去操作一片空间的申请、使用和释放叫管理
在JavaScript中申请-使用-释放
// 申请let obj = {};// 使用obj.foo = function() {}// 释放obj = null;
2 JavaScript中的垃圾回收
JavaScript的内存管理是自动的;对象不再引用是垃圾;对象不能从根上访问到时垃圾;js执行引擎识别到垃圾就会执行垃圾回收;
可达对象:可以访问到的对象就是可达对象(引用、作用域链等);可达的标准就是从根出发,是否能够被找到;JavaScript中的根就可以理解为全局变量对象;
// 定义变量后123在全局可达let obj = {foo: '123'};// 添加引用后引用计数加一let foo = obj;// 删除引用后obj还在,123依然可达foo = null;
function objGroup(obj1, obj2) {obj1.prev = obj2;obj2.next = obj1;return {o1: obj1,o2: obj2,};}let obj = objGroup({ name: 'obj1' }, { name: 'obj2' });console.log(obj)// {// o1: { name: 'obj1', prev: { name: 'obj2', next: [Circular] } },// o2: { name: 'obj2', next: { name: 'obj1', prev: [Circular] } }// }// obj通过函数使全局可达,被赋值为{o1,o2}对象// {o1,o2}对象又由o1、o2组成,o1、o2相互引用// 此时delete {o1,o2}对象的o1,并delete o2的next,o1将会失去引用,被标识为垃圾,会被回收
3 GC算法
GC就是Garbage collection的简写;GC可以找到内存中的垃圾、并释放和回收空间;
GC里的垃圾包括程序中不再需要使用的对象;
function fn1() {name = 'lg';return `${name} is a coder`}fn1(); // name 当函数调用完毕,从程序需求角度考虑,函数执行完毕,不再使用,应该要被回收
程序中不能再访问到的对象;
function fn1() {name = 'lg';return `${name} is a coder`}fn1(); // name 当函数调用完毕,从程序需求角度考虑,函数执行完毕,不再使用,应该要被回收
总结: GC是一种机制,垃圾回收期完成具体的工作;工作的内容就是查找垃圾、释放空间、回收空间;算法就是工作时查找和回收遵守的规则;
常见的GC算法,引用计数、标记清除、标记整理、分代回收;
3.1 引用计数算法
核心:设置引用数,判断当前引用数是否为0;
有引用计数器;
引用关系改变时修改引用数字;
优点:发现垃圾时立即回收;最大限度减少程序暂停(减少程序卡顿);
缺点:无法回收循环引用的对象;资源(时间)开销大;
3.2 标记清除算法
核心: 分标记和清除两个部分;先遍历所有对象标记活动对象,再遍历所有对象清除没有标记的对象,这样就可以回收空间了
过程: 从全局对象下递归遍历所有对象,标记所有除了未引用的对象,然后再遍历一遍清除未标记的对象,并清除所有标记,然后把释放的空间放到空闲链表中等待被申请;
优点:相对于引用计数算法,对于互相引用的对象可以清除;
缺点:部分情况下,释放掉的空间地址是不连续的导致空间碎片化,浪费空间;
3.3 标记整理算法
标记整理可以看做是标记清除的增强操作;标记阶段的操作与标记清除算法一致,多了一部分内容:在清除阶段会先执行整理,移动对象的位置;
优点:减少碎片化空间;
缺点:不会立即回收垃圾对象;
4 V8引擎
V8是一款主流的JavaScript执行引擎;特点:采用即时编译;内存设置上限(64位1.5G,32位800M);
4.1 V8垃圾回收策略
采用分代回收的思想;把当前内存空间分为两类:新生代、老生代;对于不同的对象采用不同的算法;
常用的GC算法:
分代回收
空间复制
标记清除
标记整理
标记增量
4.2 如何回收新生代对象
V8内存空间分两部分,小空间用于存放新生代对象(32M|16M);新生代对象是指存活时间较短的对象;
回收过程采用复制算法加标记整理;新生代空间等分为两个空间from(使用空间),to(空闲空间);
首先把活动对象存储在From空间,等空间使用达到一定程度,就开始使用GC算法中的标记整理后,将From中的活动对象拷贝到To空间;拷贝完成后From空间的活动对象就有了备份,便开始回收操作,直接释放From空间,之后From和To空间互换,这样就完成了空间的释放和回收操作;
在拷贝过程中如果发现了一轮GC后仍存活的新生代或者To空间的使用率达到25%,就会将存活的新生代对象或者To里面的活动对象移动到老生代(晋升);
4.3 如何回收老生代
老生代区域有内存大小限制(64位1.4G,32位700M);
老生代对象是指当前存活时间较久的对象(如全局作用域下的一些对象,闭包内部的一些对象);
回收过程采用标记清除、标记整理、增量标记算法
首先使用标记清除完成垃圾空间的回收;当新生代对象往老生代对象移动的过程中,如果老生代的空闲空间不足以存储移动过来的新生代对象时,就触发标记整理,会对碎片空间进行回收;最后采用增加标记的方法进行效率优化;
垃圾回收的时候会阻塞程序的执行,所以将垃圾回收分片执行;
标记增量,就是将一整段的垃圾回收操作分成多个小步骤,好让程序执行和垃圾回收交替执行;
无论哪种算法,都会先执行对象的遍历和标记动作(老生代),遍历的过程要做标记,但是标记不一定要一次做完,因为有直接可达和间接可达;在标记完直接可达对象后,先暂停垃圾标记让程序继续执行一段,然后对于二级的可达对象也进行标记,然后继续交替执行,最后完成标记以后开始回收和释放了,完成后程序继续执行;虽然程序间隔执行,但是对于最大1.5G的采用非增量回收的时间,V8也不会超过1s;
4.4 新老对比
新生代更像是用空间换时间;因为采用复制算法,每时每刻都会有一个空闲空间存在;但是新生代本来空间较小,空闲空间就更小,这部分的空间浪费相对于时间的节约,性价比更高;
老生代不适合复制算法,因为存放数据多,复制的话消耗时间更多
5 performance工具
GC的目的是为了实现内存空间的良性循环;良性循环的基础是合理使用;
performance工具提供了多种监控方式,方便时刻关注内存是否合理;
5.1 使用步骤
打开浏览器输入网址
进入开发者面板,选择performance/性能
开启录制,在地址栏回车
执行用户行为,一段时间后停止录制
分析界面中记录的内存信息
5.2 内存问题的体现(网络正常)
页面出现延迟加载和经常性的暂停;
页面持续性出现糟糕的性能;
页面的性能随着时间延长越来越差;
5.3 监控内存的几种方式
界定内存问题的标准
内存泄漏:内存使用持续升高
内存膨胀: 在大多数设备上都存在性能问题
频繁的垃圾回收:通过内存变化图进行分析
监控内存的几种方式
浏览器任务管理器
timeline时序图记录
堆快照查找分离DOM
判断是否存在频繁的垃圾回收
浏览器任务管理器监控内存
shift + esc,调出任务管理器
列表中右键勾选javascript内存
前面的内存列表示DOM占的内存,如果持续升高,说明一直有DOM操作;JavaScript内存列表示js的堆,实时内存表示所有可达对象正在使用的内存,如果持续升高,说明一直在创建新对象或者现有对象一直在增长;
该工具只能说明有问题,但是无法定位具体问题
timeline记录内存
开发者工具中性能,然后录制开始,等稳定后录制结束;勾选内存,在对应的timeline里面就可以看到具体的内存变化了;
5.4 使用堆快照查找分离DOM
界面元素都存活在DOM树上;DOM节点分为垃圾对象和分离DOM;
如果当前的DOM节点在DOM树上已经脱离,而且js代码中也没有引用的则视为垃圾;
如果当前的DOM节点在DOM树上已经脱离,但是在js代码中有引用则为分离DOM;这种情况就是内存泄漏;
打开浏览器开发者面板,找到内存,分析类型选择堆快照,执行某操作后点击快照按钮,点击生成的快照,在里面查找deta,看是否有分离DOM;如果有的话,找到对应的代码,清空节点引用;
5.5 判断是否存在频繁GC
GC工作时应用程序是停止的,频繁且过长的GC会导致应用假死;用户在使用的过程中会感知程序卡顿;
标志:Timeline中频繁的上升下降;任务管理器中数据频繁的增加减少;
6 JavaScript性能
6.1 如何精准测试JavaScript性能
本质上就是采集大量的执行样本进行数学统计和分析;
6.2 jsperf工具运用
https://jsperf.com
使用Github账号登录
填写个人信息(非必须)
填写详细的测试用例信息(title、slug)
填写准备代码
填写必要有setup和teardown代码
填写测试代码片段
6.3 慎用全局变量
全局变量定义在全局上下文,是所有作用域的顶端;
全局执行上下文一直存在于上下文执行栈,直到程序退出;
如果局部作用域中有全局同名变量,则可能造成遮蔽和污染;
6.4 缓存全局变量
将使用中无法避免的全局变量缓存到局部
var i, str;for(i = 0;i<1000;i++) {str += i}// slowerlet str =''for(let i=0;i<1000;i++) {str += i;}// faster
6.5 通过原型对象添加附加方法
在原型对象上添加实例对象需要的方法比直接定义在构造函数中,性能更优
const fn1 = function () {this.foo = function () {console.log('1111');};};let f1 = new fn1();// slowerconst fn2 = function () {};fn2.prototype.foo = function() {console.log('1111')}let f2 = new fn2();// faster
6.6 避开闭包陷阱
闭包使用不当很容易出现内存泄漏
闭包变量手动置null;
6.7 避免属性访问方法使用
JavaScript的面向对象
JavaScript不需要属性的访问方法,所有属性都是外部可见的
使用属性访问方法只会增加一层重定义,没有访问的控制力;
通过成员属性访问方法获取成员相比直接获取成员性能更低;
function Person1() {this.name= 'ss';this.getName = function() {return this.name;}}const p1 = new Person1();const a = p1.getName();// slowerfunction Person2() {this.name= 'ss';}const p2 = new Person2();const b = p2.name;// faster
6.8 for循环优化
for循环数组的长度提前获取,每次判断条件的时候不用重复获取,在数组较大时优化明显
const arr = [1,2,3,4];for(let i; i<arr.length;i++) {console.log(arr[i])}for(let i,len=arr.length; i<len;i++) {console.log(arr[i])}
6.9 选择最优的循环方式
同样的数组全部循环完毕,forEach表现更好,之后是for,之后是for in
const arr = [1,2,3,4];arr.forEach(item => {console.log(item)});for(let i,len=arr.length; i<len;i++) {console.log(arr[i])}for(let item in arr) {console.log(item)}
6.10 文档碎片优化节点添加
DOM节点的添加操作必然有回流和重绘,在一个for循环里面频繁通过appendchild添加,比先创建一个文档碎片在文档碎片里面添加之后把文档碎片appendchild进dom,性能要高;(document.createDocumentFragment)
for (let i = 0; i < 3; i++) {const op = document.createElement('p')op.innerHTML = i;document.appendChild(op);}// slowerconst fragEle = document.createDocumentFragment();for (let i = 0; i < 3; i++) {const op = document.createElement('p')op.innerHTML = i;fragEle.appendChild(op);}document.body.appendChild(fragEle);// faster
6.11 克隆优化节点操作
当想要新增节点的时候,先复制一个已有的类似节点,然后稍作修改,这样比直接创建要性能高;(cloneNode方法)
// html中已经有一个id为example的p标签for (let i = 0; i < 3; i++) {const op = document.createElement('p')op.innerHTML = i;document.appendChild(op);}// slowerconst oldP = document.getElementById('example')for (let i = 0; i < 3; i++) {const newP = oldP.cloneNode(false)op.innerHTML = i;document.appendChild(op);}// faster
6.12 直接量替换new Object
const a1 = [1,2,3,4] // fasterconst a2 = new Array(4); // slowera2[0] = 1;a2[1] = 2;a2[2] = 3;a2[3] = 4;
7 JS性能优化
7.1 JSBench 使用 jsbench,方法类似于jsperf
7.2 堆栈中的js执行过程
以下面闭包函数的执行过程为例
let a = 10;function foo(b) {let a = 2;function bar(c) {console.log(a + b + c);}return bar;}let fn = foo(2);fn(3);
首先代码执行的时候会先生成一个执行环境栈ECStack:
代码开始执行,首先会在执行环境栈中创建一个全局上下文EC(G):
首先有个变量对象VO:
执行第1行代码 a=10,原始类型直接存放到栈内存当中的;
第2行:foo = ?非原始类型,就要在重新开辟出一块堆内存来存对象(堆区),地址16进制,假设叫AB1;
AB1中存储的内容有:function foo(){….},name: // 函数名称,length: //形参个数
此时foo=?的结果就是foo的地址,foo=AB1, 函数创建的时候它会有自己的作用域[[scope]] VO
第9行: fn = ?此时遇到了函数的调用
当遇到foo的调用的时候,就会在当前环境栈中创建一个新的函数执行上下文(EC(foo))
EC(foo)里面
首先确定this指向,虽然本次调用没遇到,this=window;
然后初始化作用域链<自身作用域foo.ao(活动对象), VO>
AO:
arguments: {0: 2};
b = 2;
第3行 a = 2;
第4行 bar=? 引用类型,开辟堆内存,比如叫AB2
AB2 中存储的内容: function bar(){…}, name: bar, length:1…
此时 bar = AB2, 同时它有自己的作用域[[scope]] foo.AO
第7行 return,此时在外部EC(G)里面,fn就有了结果
按常规,此时foo执行完毕以后,空间会被回收,但是bar在外部有引用,产生闭包,此时空间不能回收
EC(G):
第9行 fn=AB2
第10行 fn(3)
函数重新执行的时候,会重现开辟一个执行上下文,EC(bar)
EC(bar)里面:
首先确定this指针 this = window;
然后作用域链
AO:
arguments: {0: 3};
c = 3;
到 a + b + c, c已知,a 当前作用域没有,然后跟着作用域链往上找foo.ao里面 a=2, b 也没有,同样在foo.ao里面找着
计算结果 a =2, b=2, c=3, a +b + c= 7;
第5行 console.log(7);
执行完毕,看要不要回收,发现没引用,就回收,这个叫出栈;
- 垃圾回收,栈内存由主线程回收,堆内存由GC垃圾回收机制来回收;
图示(来源于教材)
8 减少判断层级
多层if…else嵌套的时候,可以通过减少层级来优先性能
function fn1(part, chapter) {const parts = ['JS', 'TS', 'VUE', 'REACT', 'ANGULAR'];if (part) {if (parts.includes(part)) {console.log('属于当前模块');if (chapter > 6) {console.log('付款先');}}} else {console.log('请确认模块信息');}}fn1('JS', 7);function fn2(part, chapter) {const parts = ['JS', 'TS', 'VUE', 'REACT', 'ANGULAR'];if (!part) {console.log('请确认模块信息');return;}if (!parts.includes(part)) return;console.log('属于当前模块');if (chapter > 6) {console.log('付款先');}}fn2('JS', 7);
9 减少作用链查找层级
// 空间换时间// var a = 'sfs';// function fn() {// a = 'ddd';// function f1() {// var b = 'sff';// console.log(a)// console.log(b)// }// f1();// }// fn();var a = 'sfs';function fn() {var a = 'ddd';function f1() {var b = 'sff';console.log(a)console.log(b)}f1();}fn();
10 减少数据读取次数
<!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><button id="skip" class="skip"></button><script>const btn = document.getElementById('skip');// function hasClass(ele, glass) {// return ele.className == glass;// }// console.log(hasClass(btn, 'skip'));function hasClass(ele, glass) {const eleGlass = ele.className;return eleGlass == glass;}console.log(hasClass(btn, 'skip'));</script></body></html>
11 字面量与构造式
// let fn = () => {// let obj = new Object()// obj.name = 'Jason'// obj.age = 18// obj.sex = 'man'// return obj// };let fn =() => {let obj = {name: 'Jason',age: 18,sex: 'man',}return obj;}console.log(fn())var str1 = '哈哈哈'var str2 = new String('哈哈哈')console.log(str1)console.log(str2)
12 减少循环体中的活动
function fn() {var ivar arr = ['Jason', 18, 'live for front']for(i=0;i<arr.length;i++) {console.log(arr[i])}}function fn() {var ivar arr = ['Jason', 18, 'live for front']var len = arr.lengthfor(i=0;i<len;i++) {console.log(arr[i])}}function fn() {var arr = ['Jason', 18, 'live for front']var len = arr.lengthwhile(len--){console.log(arr[len])}}fn();
13 减少声明及语句数
<!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><div id="box"></div><script>const box = document.getElementById('box');function fn(ele) {var w = ele.clientWidth;var h = ele.clientHeight;return w * h;}function fn(ele) {return ele.clientWidth * ele.clientHeight;}fn();function f1() {var a = 'sfsf';var b = 11;var c = 'gssf';return a + b + c;}function f2() {var a = 'sfsf',b = 11,c = 'gssf';return a + b + c;}</script></body></html>
这种思路跟之前说的缓存变量有冲突?代码运行之前,会有个编译的过程,虽然js是脚本语言,但是在执行之前还是有很短暂的编译过程;这时候,类似缓存多个值的这种方法,就会有更多的语句,另外,js引擎在做词法分析的时候,遇到关键字,会按照一定的规则进行拆分,把内容变成词法单元,然后再做语法分析,组成AST语法树,有了语法树再去转成代码,最后才去执行
14 惰性函数与性能
惰性函数,函数的执行内容需要环境判断或者不变量的判断,如果后续相同的连续调用的时候每次都要进行判断就会造成浪费,所以在第一次执行判断以后,就给函数重新赋值;函数被改写,下次执行同样的函数时就没了判断的过程,直接执行;
<!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><button id="btn">点击测试</button><script>var btn = document.getElementById('btn');function foo() {console.log(this);}// function addEvent(obj, type, fn) {// if (obj.addEventListener) {// obj.addEventListener(type, fn, false);// } else if (obj.attachEvent) {// obj.attachEvent('on' + type, fn);// } else {// obj['on' + type] = fn;// }// }function addEvent(obj, type, fn) {if (obj.addEventListener) {addEvent = (obj, type, fn) => obj.addEventListener(type, fn, false);} else if (obj.attachEvent) {addEvent = (obj, type, fn) => obj.attachEvent('on' + type, fn);} else {addEvent = (obj, type, fn) => obj['on' + type] = fn;}return addEvent;}addEvent(btn, 'click', foo)addEvent(btn, 'click', foo)addEvent(btn, 'click', foo)</script></body></html>
15 事件委托
<!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><ul id="outer"><li>第一个</li><li>第二个</li><li>第三个</li></ul><script>// var list = document.querySelectorAll('li');// function showText (ele) {// console.log(ele.target.innerHTML)// }// for(let item of list){// item.onclick = showText// }var ul = document.getElementById('outer');ul.addEventListener('click', showText, true);function showText(ele) {var obj = ele.target;if(obj.nodeName.toLowerCase()==='li') {console.log(obj.innerHTML)}}</script></body></html>
