1.内存管理
-
内存管理介绍
-
内存:由可读写单元组成,表示一片可操作空间
-
管理:人为的去操作一片空间的申请、使用和释放
-
内存管理:开发者主动申请空间、使用空间、释放空间
-
管理流程:申请 - 使用 - 释放
-
-
js中的内存管理
// 申请空间
let obj = {}
// 使用空间
obj.name = 'lg'
// 释放
obj = null
2.JavaScript 中的垃圾回收
-
JavaScript 中的垃圾
-
JavaScript 中内存管理是自动的
-
对象不再被引用时是垃圾
-
对象不能从根本上访问到时是垃圾
-
-
JavaScript 中的可达对象
-
可以访问到的对象就是可达对象(引用、作用域链)
-
可达的标准就是从根出发是否能够被找到
-
JavaScript 中的根就可以理解为是全局变量对象
-
3.GC算法
3.1.概念
-
GC就是垃圾回收机制的简写
-
GC可以找到内存中的垃圾、释放并回收空间
-
GC里的垃圾是什么
- 程序中不再需要使用的对象
function func() {
name = 'lg'
return `${name} is a coder`
}
func()
- 程序中不能再访问到的对象
function func() {
const name = 'lg'
return `${name} is a coder`
}
func()
-
GC算法是什么
-
GC是一种机制,垃圾回收器完成具体的工作
-
工作内容就是查找垃圾、释放并回收空间
-
算法就是工作时查找和回收所遵循的规则
-
-
常见GC算法
-
引用计数
-
标记清除
-
标记整理
-
分代回收
-
3.2.引用计数算法
-
实现原理:
-
核心思想:设置引用数,判断当前引用数是否为0
-
引用计数器
-
引用关系改变时修改引用数字
-
引用数字为0时立即回收
-
示例:
-
const user1 = { age: 11 }
const user2 = { age: 22 }
const user3 = { age: 33 }
const nameList = [user1.age, user2.age, user3.age]
function fn() {
const num1 = 1
const num2 = 2
}
fn()
-
优点:
-
发现垃圾时立即回收
-
最大限度减少程序暂停(卡顿)
-
-
缺点:
-
无法回收循环引用的对象
-
时间开销大
-
// obj1 和 obj2是循环引用对象
function fn() {
const obj = {}
const obj2 = {}
obj1.name = obj2
obj2.name = obj1
return 'lg is a coder'
}
fn()
3.3.标记清除算法
-
实现原理:
-
核心思想:分标记和清除两个阶段完成
-
遍历所有对象找标记活动对象
-
遍历所有对象清除没有标记对象
-
回收相应的空间
-
-
优点:
- 解决对象循环引用不能回收的问题
-
缺点:
- 释放的空间不连续,也就是空间碎片化
3.4.标记整理算法
-
实现原理:
-
标记整理可以看做是标记清除的增强
-
标记阶段的操作和标记清除一致
-
清除阶段会先执行整理,移动对象位置
-
回收前:
整理后:
回收后:
4.V8
4.1.概念
-
V8 是一款主流的 JavaScript 执行引擎
-
V8 采用即时编译
-
V8内存设限
4.2.垃圾回收策略
-
采用分代回收的思想
-
内存分为新生代和老生代
-
针对不同对象采用不同算法
-
常用的GC算法
-
分代回收
-
空间复制
-
标记清除
-
标记整理
-
标记增量
-
4.3.如何回收新生代对象
-
V8内存分配
-
V8内存空间一分为二
-
小空间用于存储新生代对象(32M | 16M)
-
新生代指的是存活时间较短的对象
-
-
新生代对象回收实现
-
回收过程采用复制算法 + 标记算法
-
新生代内存区分为两个等大小的空间
-
使用空间为From,空间空间为To
-
活动对象存储于From空间
-
标记整理后将活动对象拷贝至To
-
From空间进行释放
-
-
回收细节说明
-
拷贝过程中可能出现晋升
-
晋升就是将新生代对象移动到老生代
-
一轮GC后还存活的新生代需要晋升
-
To空间的使用率超过25%
-
-
-
4.4.如何回收老生代对象
-
老生代对象说明
-
老生代对象存放在右侧老生代区域
-
64位操作系统1.4G,32位操作系统700M
-
老生代对象就是指存活时间较长的对象
-
-
老生代对象回收实现
-
主要采用标记清除、标记整理、增量标记算法
-
首先使用标记清除完成垃圾空间的回收
-
新生代晋升时如果发现老生代区域不足,采用标记整理进行空间优化
-
采用增量标记进行效率优化
-
-
新老生代细节对比
-
新生代区域垃圾回收使用空间换时间
-
老生代区域垃圾回收不适合复制算法
-
-
标记增量:
5.Performance
5.1.为什么使用Performance
-
GC 的目的是为了实现内存空间的良性循环
-
良性循环的基石是合理利用
-
时刻关注才能确定是否合理
-
Performance 提供了多种监控方式
5.2.Performance使用步骤
-
打开浏览器输入目标网址
-
进入开发人员工具面板,选择性能
-
开启录制功能,访问具体页面
-
执行用户行为,一段时间后停止录制
-
分析界面中记录的内存信息
5.3.内存问题的体现
-
页面出现延迟加载或经常性暂停(频繁GC)
-
页面持续性出现糟糕的性能(内存膨胀)
-
页面的性能随时间延长越来越差(内存泄漏)
5.4.监控内存的几种方式
-
界定内存问题的标准
-
内存泄漏:内存使用持续升高
-
内存膨胀:在多数设备上存在性能问题
-
频繁GC:通过内存变化图进行分析
-
-
监控内存的方式
-
浏览器任务管理器
-
Timeline时序图记录
-
堆快照查找分离DOM
-
判断是否存在频繁的垃圾回收
-
5.5.任务管理器监控内存
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>任务管理器监视内存变化</title>
</head>
<body>
<button id="btn">Add</button>
<script>
const oBtn = document.getElementById('btn')
oBtn.onclick = function () {
let arrList = new Array(1000000)
}
</script>
</body>
</html>
在浏览器打开,使用快捷键 Shift + Esc,打开任务管理器,右键选择JavaScript内存选项,点击Add按钮,会发现JavaScript内存会增大
5.6.Timeline记录内存
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>时间线记录内存变化</title>
</head>
<body>
<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(10000).join('x'));
}
document.getElementById('btn').addEventListener('click', test)
</script>
</body>
</html>
打开浏览器,选择性能,录制后可以查看
5.7.堆快照查找分离DOM
-
什么是分离DOM
-
界面元素存活在DOM树上
-
垃圾对象时的DOM节点
-
分离状态的DOM界面
-
-
示例代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>堆快照监控内存</title>
</head>
<body>
<button id="btn">Add</button>
<script>
var tmpEle
function fn(){
var ul = document.createElement('ul')
for (var i = 0; i < 10; i++) {
var li = document.createElement('li')
ul.appendChild(li)
}
tmpEle = ul
}
document.getElementById('btn').addEventListener('click', fn)
</script>
</body>
</html>
打开浏览器,选择内存选项,点击页面中的Add按钮,然后获取快照,筛选deta关键字,就可以看到分离DOM
这些分离DOM会消耗内存,所以在我们确定他们没有用的时候,需要将其引用置空tmpEle = null
,避免内存浪费。
5.8.判断是否存在频繁GC
-
为什么要判断
-
GC工作时应用程序是停止的
-
频繁且过长的GC会导致应用假死
-
用户使用中感知应用卡顿
-
-
判断方式
-
Timeline中频繁的上升下降
-
任务管理器中数据频繁的增加减小
-
6.V8引擎
6.1.执行流程
-
Scanner是扫描器,扫描所有的文件,并且进行一次tokens转化(词法分析)
-
预解析(PreParser)优点:
-
跳过未使用的代码
-
不生成AST,创建无变量引用和声明的scopes
-
依据规范抛出特定错误
-
解析速度更快
-
-
全量解析(Parser):
-
解析被使用的代码
-
生成AST
-
构建具体scopes信息,变量引用、声明等
-
抛出所有语法错误
-
// 声明时未调用,因此会被认为是不被执行的代码,进行预解析
function foo() {
console.log('foo')
}
// 声明时未调用,因此会被认为是不被执行的代码,进行预解析
function fn() {}
// 函数立即执行,只进行一次全量解析
(function bar(){
})()
// 执行foo, 那么需要重新对foo函数进行全量解析,此时foo函数被解析了两次
foo()
-
Ignition:是V8提供的一个解释器,AST转为字节码
-
TurboFan 是VB提供的编译器模块,字节码转为机器码
6.2.堆栈处理
-
堆栈准备
-
JS 执行环境
-
执行环境栈(ECStack,execution context stack)
-
执行上下文
-
VO(G),全局变量对象
-
6.3.引用类型堆栈处理
6.4.函数堆栈处理
6.5.闭包堆栈处理
6.6.闭包与垃圾回收
继续向下执行
6.7.循环添加事件实现
- 代码实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>循环添加事件</title>
</head>
<body>
<button index="1">按钮1</button>
<button index="2">按钮2</button>
<button index="3">按钮3</button>
<script src="./15-add-event-loop.js"></script>
</body>
</html>
// 15-add-event-loop.js
var aButtons = document.querySelectorAll('button')
// 有问题的
// for(var i = 0; i < aButtons.length; i++) {
// aButtons[i].onclick = function (){
// console.log(`当前索引值为${i}`)
// }
// }
// 解决1:闭包
for(var i = 0; i < aButtons.length; i++) {
(function (i){
aButtons[i].onclick = function (){
console.log(`当前索引值为${i}`)
}
})(i)
}
// 解决2:闭包2
for(var i = 0; i < aButtons.length; i++) {
aButtons[i].onclick = (function (i){
return function (){
console.log(`当前索引值为${i}`)
}
})(i)
}
// 解决3:let
for(let i = 0; i < aButtons.length; i++) {
aButtons[i].onclick = function (){
console.log(`当前索引值为${i}`)
}
}
// 解决4:添加自定义属性
for(var i = 0; i < aButtons.length; i++) {
aButtons[i].myIndex = i
aButtons[i].onclick = function (){
console.log(`当前索引值为${this.myIndex}`)
}
}
// 解决5:事件委托
document.body.onclick = function (ev) {
var target = ev.target
var targetDom = target.tagName
if(targetDom === 'BUTTON') {
var index = target.getAttribute('index')
console.log(`当前点击的是第${index}个`)
}
}
- 底层执行分析:
有问题的写法分析
闭包写法分析
添加自定义属性的写法分析
7.JavaScript性能优化
7.1. 测试工具JSBench
网站地址:https://jsbench.me/
7.2. 变量局部化
这样可以提高代码的执行效率(减少数据访问时需要查找的路径)
7.3.缓存数据
对于需要多次使用的数据进行提前保存,后续进行使用,作用域链查找变快
7.4.减少访问层级
7.5.防抖与节流
-
为什么需要防抖和节流
- 在一些高频率事件触发的场景下,我们不希望对应的时间处理函数多次执行
-
场景:
-
滚动事件
-
输入的模糊匹配
-
轮播图切换
-
点击操作
-
-
浏览器默认情况下都会有自己的监听事件间隔(4~6ms),如果检测到多次事件的监听执行,那么就造成不必要的资源浪费。
-
前置场景:界面上有一个按钮,我们可以连续多次点击
-
防抖:对于这个高频的操作来说,我们只希望识别一次点击,可以认为是第一次或者最后一次。
-
节流:对于高频操作,我们可以自己来设置频率,让本来会执行很多次的事件触发,按照我们定义的频率减少触发的次数。
-
防抖函数实现:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>防抖函数实现</title>
</head>
<body>
<button id="btn">点击</button>
<script>
var oBtn = document.getElementById('btn')
/**
* 防抖函数:
* handle 最终需要执行的事件监听
* wait 事件触发之后多久开始执行
* immediate 控制执行第一次还是最后一次,false 执行最后一次
* */
function myDebounce(handle, wait, immediate){
// 参数类型判断及默认值处理
if(typeof handle !== 'function') throw new Error('handle must be a 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
clearTimeout(timer)
timer = setTimeout(() => {
timer = null
!immediate ? handle.call(self, ...args): null
}, wait)
// 如果immediate是true,那就立即执行
// 如果想要实现只在第一次执行,那init的赋值就再加上timer为null
// 因为只要timer为null,那么就意味着没有第二次点击
init ? handle.call(self, ...args): null
}
}
// 定义事件执行函数
function btnClick(ev){
console.log('点击了', this, ev)
}
// 当我们执行了按钮点击之后就会执行
oBtn.onclick = myDebounce(btnClick, 200, true)
</script>
</body>
</html>
节流函数实现:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>节流函数实现</title>
<style>
body{
height: 5000px;
}
</style>
</head>
<body>
<script>
// 节流:在自定义的一段时间内让事件进行触发
function myThrottle(handle, wait) {
if(typeof handle !== 'function') throw Error('handle must be an function')
if(typeof wait === 'undefined') wait = 400
let previous = 0 // 定义变量记录上一次的执行事件
let timer = null // 管理定时器
return function proxy(...args) {
let now = new Date() // 定义变量记录当前次执行时刻的时间点
let self = this
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(() => {
clearTimeout(timer) // 这个操作只是将系统中的定时器清除了,但是timer中的值还在
timer = null
handle.call(self, ...args)
previous = new Date()
}, interval)
}
}
}
// 定义滚动事件监听
function scrollFn() {
console.log('滚动了')
}
// window.onscroll = scrollFn
window.onscroll = myThrottle(scrollFn, 600)
</script>
</body>
</html>
7.6.减少判断层级
function doSomeThing(part, chapter) {
const parts=['ES2016', '工程化', 'React', 'Node']
if(part) {
if(parts.includes(part)){
console.log('属于当前前端课程')
if(chapter > 5) {
console.log('您需要提供VIP身份')
}
}
} else {
console.log('请确认模块信息')
}
}
doSomeThing('ES2016', 6)
// 优化
function doSomeThing(part, chapter) {
const parts=['ES2016', '工程化', 'React', 'Node']
if(!part) {
console.log('请确认模块信息')
return
}
if(!parts.includes(part)) return
console.log('属于当前前端课程')
if(chapter > 5) {
console.log('您需要提供VIP身份')
}
}
doSomeThing('ES2016', 6)
7.7.减少循环体活动
var test = () => {
var i
var arr = ['zxc', 38, '前端']
for (i = 0; i < arr.length; i++) {
console.log(arr[i])
}
}
test()
// 优化1
var test = () => {
var i
var arr = ['zxc', 38, '前端']
var len = arr.length
for (i = 0; i < len; i++) {
console.log(arr[i])
}
}
test()
// 优化2
var test = () => {
var arr = ['zxc', 38, '前端']
var len = arr.length
while(len--) {
console.log(arr[len])
}
}
test()
7.8.字面量与构造式
let test = () => {
let obj = new Object()
obj.name = 'zxc'
obj.age = 18
obj.slogan = '前端'
return obj
}
console.log(test())
// 优化
let test = () => {
let obj = {
name: 'zxc',
age: 18,
slogan: '前端'
}
return obj
}
console.log(test())
var str2 = new String('zxc')
// 优化
var str1 = 'zxc'
标签:function,console,log,性能,JavaScript,回收,内存,var,优化
From: https://www.cnblogs.com/caicai521/p/17036024.html