本次学习为 javascript 语言本身的优化
内容概要 :
function fn () {
arrList = []
arrList[10000] = 'lg is a coder'
}
fn()
选择比较大的下标是为了申请一块比较大的内存,代码在运行fn时内存占用是逐渐层高而未回落, 说明这个函数存在内存泄漏,而在代码中内存泄漏的代码多了会形成意向不到的bug, 所以内存管理是很有必要的。
内存 : 由可读写单元组成,表示一片可以操作空间。
管理: 人为的去操作一片空间的申请 使用和释放。
内存管理 : 开发者主动去申请空间 使用空间 和释放空间。
管理流程: 申请 -> 使用 -> 释放
javascript 中的内存管理主要依靠垃圾回收机制,因为 ECMAScript 中并没有提供 API 来主动操作管理流程。
// 定义一个变量来申请内存空间
let obj = {}
// 使用内存空间
obj.name = "lg"
// 释放内存空间
obj.name = null
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] } }
}
function fn() {
name = 'lg'
return `${name} is a coder`
}
fn()
function fn() {
const name = 'lg'
return `${name} is a coder`
}
常见的 GC 算法 引用计算 标记清除 标记整理 分代回收。
引用关系改变时修改引用数值 有对该对象的引用时引用数值加 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()
优点
缺点
function fn() {
const obj1 = {}
const obj2 = {}
// obj1 和 obj2 fn 执行结束后在根上已经找不到这两个对象了 , 但是也没有被回收因为下面代码仍对它们有引用而垃圾回收机制是没办法进行回收 obj1 和 obj2 的因为它们处在循环引用中。
obj1.name = obj2
obj2.name = obj1
return 'ok'
}
fn()
优点
缺点
空间碎片化: 当前回收的垃圾对象在地址上时不连续的, 由不连续所以空闲空间会分散, 如果新申请的空间和碎片化空间匹配
自然好,如果不匹配就不适合使用了。
设限原因:
V8 常用的 GC 算法 :分代回收 空间赋值 标记清除 标记整理 标记增量
回收采用复制算法 + 标记整理算法。
新生代内存区分为两个等大的空间, 使用空间为 From 空闲空间为 To, 将活动对象存储在 From 空间中, 当 From 空间达到定饱和度的时候就会进行标记整理。
将标记整理后的活动对象拷贝至 To 空间, 然后开始回收,回收的体现就是 From 空间和 To 空间的交换完成释放。
回收细节说明:
拷贝过程中可能出现晋升 : 就是在拷贝的过程中发现一个对象 在老生代中也存在 ,就会出现一个晋升的操作,也就是把新生代的对象移动至老生代进行村存储。
触发晋升操作场景:
注: 如果 To 空间中的使用率超过 80 % 后 From 空间和 To 空间交换之后 新的活动对象可能就存不进当前的 From 对象中了。
老生代对象说明
新生代区域垃圾会后使用空间交换时间, 新生代采用空间交替的方法,所以实时都会有空闲的空间,造成空间浪费
老生代区域垃圾回收不适合赋值算法, 老生代中的对象比较多,而且老生代空间比新生代要大的多,如果实时都有空闲的太过浪费
为什么使用 Performance 工具 ?
GC 算法的目的就是为了实现内存精简的良性循环,良性循环的基石就是合适使用内存空间, 但是 javascript 并没有提供这系列的 API 来进行操作。我们想去判断内存使用是否合理,需要时刻关注内存使用的变化,而 Performance 这个工具就可以让我们可以时刻关注到内存使用的变化,以便于定位由内存使用有问题的代码块。
配合 Performance 定位问题
页面出现延迟加载或者经常性的暂停(网络环境正常且频繁操作 GC 的垃圾回收) :意味着代码中有些代码瞬间让内存爆掉了,触发了 GC 垃圾回收机制。
页面持续性出现糟糕的性能(底层出现内存膨胀)。
内存膨胀:当前界面为了达到最佳使用速度去申请内存空间 ,申请的空间大小超过了设备提供的大小。
内存泄漏: 内存使用持续升高 可以拿到内存走势图可以从走势图中看到
内存膨胀: 在多数设备上都存在性能问题 有可能是当前设备不具备申请空间的条件 应该多更换其他设备进行测试 如果多设备上都存在性能问题,则为内存膨胀
频繁垃圾回收: 通过内存变化图进行分析 界面无法感知
判断是否存在频繁的垃圾回收
作用 :具体定位内存出现问题的时间和代码
模拟场景:创建大量 dom 节点模拟内存消耗和数组遍历存入大量数据
什么是分离 DOM
找到 js 堆产生照片的留存,对分离 dom 的查找行为。
操作: 打开浏览器中的 memory 点击 take snapshot 拍摄当前的堆快照, 在新增 dom 后拍摄新增后的堆快照, 新增后的堆快照可以通过搜索 deta 确认是否存在分离 dom
如何确定频繁垃圾回收?
GC 工作时应用程序会停止,频繁且过长的 GC 会导致应用假死,用户使用中感知应用卡顿。
jsperf 的使用流程
程序执行过程中如果有些数据需要存储尽量使用局部作用域中变量进行存储。
为什么要慎用?
var i, str = ''
for (i = 0; i < 1000; i++) {
str += i
}
for (let i = 0; i < 1000; i++) {
let str = ''
str += i
}
使用 jsperf 测试哪一段代码的执行速度更高
结果局部变量的代码执行速度远高于全局变量
缓存全局变量: 将使用中无法避免的全局变量缓存到局部。
使用两种方法写入代码使用 jsperf 去测试两端代码的运行速度。
11111
22222
33333
结果是使用局部缓存全局变量的运行速度更快。
javascript 中有构造函数,原型对象,实例对象三种概念。
通过原型对象添加附加方法:如果某实例对象需要频繁使用一个方法可以在原型对象上新增实例对象需要的方法
方法挂载到原型上和普通构造函数上的运行速度比较
var fn1 = function () {
this.too = function () {
console.log('111');
}
}
let f1 = new fn1()
var fn2 = function () {
fn2.prototype.foo = function () {
console.log('1111');
}
}
闭包的特点:外部具有指向内部的引用, 在外部作用域访问内部作用域的数据 。
function foo() {
var name = 'zs'
function fn() {
console.log(name);
}
return fn
}
var a = foo()
a()
关于闭包
js 不需属性的访问方法, 所有属性都是外部可见的
属性访问方法的运行速度并不入直接访问属性来的快
for 循环中优化为把数组的 length 用变量声明使用 jsperf 来看下两个执行速度.
add
add
add
add
add
add
add
add
add
add
add
结果存储 length 速度要快于普通的 for 循环。
如何判断最优循环方式:大量数据遍历,遍历的方法有很多.
可以通过一段逻辑相同的代码使用不同的遍历方法来看下哪个循环方式是最优的
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。
操作 dom 是非常耗性能的,这个过程必然会有回流和重绘
// 第一种方式直接量
var a = [1, 2, 3]
// 第二种new创建数组根据下标写入
var a1 = new Array(3)
a1[0] = 1
a1[1] = 2
a1[2] = 3
直接量的速度或者说执行效率要优先。
为什么学习JSBench使用?
JSBench页面介绍:
1. SetUp是指比较代码前准备工作,比如有html和js 把代码放进相应的SetUpHTML和SetUpJS当中
2. TestCase里面放要比较的js代码 enter test case name 放标题
3. TeardownJS中放入类似共同调用的函数方法之类的公共的比较js代码后续处理工作的js
4. Output(DOM)里面进行dom操作一般是不需要使用的, 下面是有RUN按钮用来开始执行比较的js代码。
注意点:
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)
上段代码执行过程:
VO:
存储 a = 10
foo = AB1[[scope]] VO
fn = AB2
fn(3)
foo = AB1 [[scope]] VO
AB1中:
function foo (b) { ... }
name: foo
length:1
this = window <
foo.ao, vo >
AO:
Arguments: [0, 2]
b = 2
a = 2
baz = AB2 [[scope]] foo.AO
function baz(c) { ... }
name: baz
length:1
this = window
< baz.ao, foo.ao, vo >
AO:
/ Arguments: [0: 3]
// c = 3
console.log(a + b + c)
3+2+2 ===> 7
表现为使用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)
关注为什么减少层级以及减少层级怎么达到优化代码的 ?
// 关注内存的情况下 这种情况会更佳
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()
从不同的角度来说各有各的优势在自己的项目中需要取舍
函数执行中会产生一个执行上下文 多次调用同一个函数会创造多个执行上下文,在执行函数过程中会先在自己的执行上下文中去找需要的变量, 如果没有找到则找上一级。
关注为什么要去减少数据读取次数,以及减少数据读取次数后对性能的影响?
减少数据读取次数即为尽可能减少对象成员的查找次数以及嵌套层级和嵌套深度。
方法就是提前缓存对象数据
需求: 进入一般都有一个广告的欢迎页页面, 上面有跳过按钮, 判断当前是否有跳过按钮, 如果没有进行其他操作
对比结果为优化后执行速度更快。
惰性函数被看为是高阶的语法 反而执行速度没有更快
var btn = document.getElementById('btn')
function foo() {
console.log(this); //
}
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)
事件委托的本质就是利用js的冒泡机制把本来需要给子元素注册的事件委托给父元素来完成事件监听,
优点:
考点一般就是给一堆li不考虑内存占用给每个li都绑定事件达到性能最优
- ZCE
- 28
- 世界很大想去看看