正所谓底层基础决定上层建筑,如果想写出优雅高性能的前端应用,就必须了解 JS的内存机制
。他会帮助你理解譬如:闭包、深浅复制、引用数据类型和引用传递;
堆
比栈
大,栈比堆的运算速度快,
如:数组
可以无限扩充,对象
可以自由添加属性。将他们放在堆中是为了不影响栈的效率,而是通过引用的方式查找到堆中的实际对象再进行操作。
相对于**简单数据类型
而言,简单数据类型就比较稳定**,并且它只占据很小的内存。不将简单数据类型放在堆是因为通过引用到堆中查找实际对象是要花费时间的,而这个综合成本远大于直接从栈中取得实际值的成本。所以简单数据类型的值直接存放在栈中。
记住一句话:能量是守衡的,无非是时间换空间,空间换时间的问题
1、JS内存周期
内存分配
==> 内存使用
==> 内存回收
- 内存分配:当我们申明变量、函数、对象的时候,系统会自动为他们分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存
2、js数据类型
谈到js内存,我们从数据类型来作为切入口帮助理解,js数据类型分两大类,分别是基本数据类型和引用数据类型
3、 js内存空间:堆、栈、队列:
堆、栈、队列之间的区别是?
①
堆
是在程序运行时,而不是在程序编译时,申请某个大小的内存空间。即动态分配内存,对其访问和对一般内存的访问没有区别。
②栈
就是一个桶,后放进去的先拿出来,它下面本来有的东西要等它出来之后才能出来。(后进先出)
③队列
只能在队头做删除操作,在队尾做插入操作.而栈只能在栈顶做插入和删除操作。(先进先出)
4、了解事件循环 Even Loop
JavaScript的运行机制
是指浏览器或Node
的一种解决javaScript单线程运行时不会阻塞
的一种机制
,也就是我们经常使用异步
的原理。
所谓Event Loop,就是事件循环,其实就是JS管理事件执行的一个流程,具体的管理办法由他具体的运行环境确定。目前JS的主要运行环境有两个,浏览器和Node.js。这两个环境的Event Loop还有点区别,我们会分开来讲:
5、浏览器中的 Event Loop
Javascript
有一个 main thread 主线程
和 call-stack 调用栈(执行栈)
,所有的任务都会被放到调用栈等待主线程执行。
Javascript单线程任务被分为同步任务
和异步任务
,事件循环就是一个循环,是各个异步线程用来通讯和协同执行的机制。各个线程为了交换消息,还有一个公用的数据区,这就是事件队列
。
1、同步任务会在调用栈中按照顺序等待主线程依次执行
2、异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中
2、等待主线程空闲的时候(调用栈被清空),就来看看这个队列有没有新活儿,有的话就取出来执行。画成一个流程图就是这样:
记录:
- JS调用栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。
- 任务队列Task Queue,即队列,是一种先进先出的一种数据结构。
流程讲解如下:
1、 主线程每次执行时,先看看要执行的是同步任务,还是异步的API
2、 同步任务就继续执行,一直执行完
3、遇到异步API就将它交给对应的异步线程,自己继续执行同步任务
4、异步线程执行异步API,执行完后,将异步回调事件放入事件队列上
5、主线程手上的同步任务干完后就来事件队列看看有没有任务
6、主线程发现事件队列有任务,就取出里面的任务执行
7、主线程不断循环上述流程
其中:任务队列里的事件可以分为 宏任务和微任务。
注意:微任务拥有更高的优先级
1、当事件循环遍历队列时,先检查微任务队列
2、如果里面有任务,就全部拿来执行,执行完之后再执行一个宏任务。
3、执行每个宏任务之前都要检查下微任务队列是否有任务,如果有,优先执行微任务队列。
所以完整的流程图如下:
注意:
1、一个Event Loop可以有一个或多个事件队列,但是只有
一个微任务队列
。
2、微任务队列全部执行完会重新渲染一次
3、每个宏任务执行完都会重新渲染一次
4、requestAnimationFrame处于渲染阶段,不在微任务队列,也不在宏任务队列
- MacroTask(宏任务)
script全部代码
(可以理解为外层同步代码)、setTimeout/setInterval
、setImmediate
(Node.js,浏览器暂时不支持,只有IE10支持,具体可见MDN)、I/O
、UI Rendering
、postMessage
。
- MicroTask(微任务)
Process.nextTick
(Node独有)、Promise
、Object.observe
(废弃)、MutationObserver
同步任务 ==> 微任务 ==> 宏任务
来个例子:
// 同步任务
console.log('script start')
// async/await 在底层转换成了 promise 和 then 回调函数,可以说是 promise 的语法糖
// 微任务
async function async1() {
await async2()
console.log('async1 end')
}
// 微任务
async function async2() {
console.log('async2 end')
}
async1()
// 宏任务
setTimeout(function() {
console.log('setTimeout')
}, 0)
// 微任务
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
// 同步任务
console.log('script end')
分析这个实例(一定要注意这个步骤,个人觉得挺难的,我是按结果推的这个,很复杂):
这里还要注意一个面试的考点:
**考点:Promise的executor以及then的执行方式**
**回答:**这个要从Promise的实现来说,Promise的executor是一个同步函数,即非异步,立即执行的一个函数,因此他应该是和当前的任务一起执行的。而Promise的链式调用then,每次都会在内部生成一个新的Promise,然后执行then,在执行的过程中不断向微任务(microtask)推入新的函数,因此直至微任务(microtask)的队列清空后才会执行下一波的macrotask。
分析:
- console.log(‘script start’); 执行 (1)
script start
- 定义异步函数 async1, 异步函数 async2
- 执行 async1(), 执行 async1() 中的 await async2(): 打印 (2)
async2 end
; 遇到 await 后面的函数进入任务队列,这里又注册一个微任务(我们标记为 mico1);到这里 async1() 就执行完了- setTimeout 执行,异步放入异步队列中,注意这是一个宏任务(我们标记为 macro1)
- 执行 new Promise:打印 (3)
Promise
,执行 resolve(); 然后在 then 中注册回调函数,console.log(‘promise1’) 函数进入任务队列;注册 event queue(我们标记为
mico2).这里 new Promise 就执行完了。- 执行 console.log(‘script end’);, 打印 (4)
script end
;- 上面👆五步把主线程都执行完毕了,然后去event queue 查找有没有注册的函数; 我们发现了(macro 1, mico1, mico2),按照优先执行微任务的原则,我们按照这样的顺序执行 mico1 > mico2 > macro1。 打印:(5)
async1 end
(6)promise1
- mico2执行结束,然后在 then 中注册回调函数,console.log(‘promise2’) 函数进入任务队列;注册 event queue(我们标记为 mico3).这里 new Promise 就执行完了。
- 这时候任务队列里有(macro1、mico3),按照优先执行微任务的原则,我们按照这样的顺序执行mico3 > macro1 打印:(7)
promise2
(8)setTimeout
也不知道想法思路对不对,有问题的可以在评论区指出来!!!!
运行结果附上(供各位参考):
注意:可能你会在不同浏览器发现不同结果,这是因为不同浏览器和版本的不同遵循的 promise 规则不同。这里是按照较新版本的 chrome(68+) 执行的结果,其他版本大家可以看这篇文章
这里在提供给大家一道题:
console.log('1');
new Promise((resolve, reject) => {
console.log('2');
resolve();
}).then(() => {
console.log('5');
}).then(() => {
console.log('7');
return new Promise(resolve => {resolve(); })
}).then(() => {
console.log('9-----p1wai');
})
new Promise((resolve, reject) => {
console.log('3');
resolve();
}).then(() => {
console.log('6');
}).then(() => {
console.log('8');
}).then(() => {
console.log('10');
}).then(() => {
console.log('11');
}).then(() => {
console.log('12--p2wai');
}).then(() => {
console.log('13--p2wai');
}).then(() => {
console.log('14');
})
console.log('4');
运行结果:
大家可以思考分析一下9-----p1wai
的打印位置,欢迎评论区发表你的见解!!!!!!
参考:可以研究一下promise的原理,promise then中返回一个promise 的处理方式,最主要是resolvePromise这个方法,promise中对于then中返回一个promise的处理方式,因为有一些回掉的处理,因此跳出原来的队列。
6、NodeJS的Event Loop
Node的Event loop一共分为6个阶段
,每个细节具体如下:
timers
: 执行setTimeout和setInterval中到期的callback。pending callback
:上一轮循环中少数的callback会放在这一阶段执行。 idle, prepare: 仅在内部使用。poll
:最重要的阶段,执行pending callback,在适当的情况下回阻塞在这个阶段check
:执行setImmediate(setImmediate()是将事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行setImmediate指定的回调函数)callback。close callbacks
:执行close事件的callback,例如socket.on(‘close’[,fn])或者http.server.on('close,fn)。
具体细节大家可以参考这篇文章