堆栈和队列涉及到计算机底层原理,没有计算机基础的更需要学习一下。
前几篇沉淀了变量、函数,但是他们在计算机是如何存储运行的,本篇沉淀的内容就是前几篇的知识点的底层分析。
MDN的一张图比较形象
其中 Stack
是“栈”,Heap
是“堆”,Queue
是队列。
在计算机领域,堆栈是一种数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除。
栈
下面是栈的模型,在百度百科和教科书上常见的模型图
栈就像一个容器,不断往里面放东西,取东西的时候只能从上面取。
栈是一种只能在一端进行插入和删除操作的特殊线性表。因此,栈的执行逻辑是后进先出
栈一般都是已知大小或者有范围上限的,由编译器自动分配释放,一般存储基本数据类型。
栈在JS中的场景分析:
基础数据类型
基本数据类型是直接报错在栈区内的。访问方式是按值访问。
var a = 1; // 栈内新增一条
var b = a; // 首先在栈中创建一个新值(1),然后把值复制到b的位置上。
b = 2; // 改变b在栈内的值
上述代码的执行过程就是为什么基础数据类型不是引用传递的原因。
复杂数据类型
复杂数据类型(Object、Array)保存的实际上的只是一个指针,这个指针指向内存中的另一个位置,该位置保存着对象。访问方式是按引用访问。
var a = {}; // 栈内新增一条,值为引用指针
var b = a; // 拷贝a的值(引用指针),然后把值复制到b的位置上。
b.key = 1; // 改变值时,找到栈内的b,通过引用指针找到并改变值,由于a也指向这个值,所以a也改变了
上述代码的执行过程就是为什么复杂数据类型是引用传递的原因。
但是,为什么直接给b重新赋值就a就不会跟着改变了,其底层执行逻辑:
var a = {}; // 栈内新增一条,值为引用指针
var b = a; // 拷贝a的值(引用指针),然后把值复制到b的位置上。
b = { key: 1 }; // 在内存中创建新的值,将b的引用指针指向新的值。
由于重新赋值时是新的值,而不是改变原有值,所以会在内存新开辟内存存储,这时a和b的指针指向的不是同一个值内存,也就不会跟着改变了。
堆
上面有部分内容已经说到堆了,现在具体总结一下堆。
栈是有序的存储,后进先出。
堆是无序的存储,基于key直接获取值。
需要注意的是,虽然引用类型的值存在堆中,但是地址指针是存储于栈中的
队列
栈是后进先出
,而队列则是先进先出
。栈像一个盒子,那么队列则像一个管道。
JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。
运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。
函数执行过程
对于数据类型比较简单,很容易了解其存储原理,但是函数比较复杂,包含逻辑和变量,那么它是怎么存储的呢?
function sum(a, b) {
return a + b;
}
上述函数是比较简单的函数,包含了本次需要分析的所有点,具体过程:
- 存储:因为函数也属于对象,所以创建函数时会在堆区分配内存存储函数(字符串),在栈区中存储函数的引用。
- 运行:创建执行上下文栈环境,取出函数字符串在当前执行上下文环境进行运行。
- 释放:根据实际情况判断当前上下文是否出栈释放
函数的运行比较复杂,上面简述了过程,具体过程在文尾引入了写的比较细的博文,可以具体了解。
事件循环(EventLoop)
事件循环的本质就是队列的执行过程。理解队列就会理解事件循环,下面沉淀一下事件队列的过程。
首先。队列会添加同步任务,然后执行一个个任务
console.log(1);
console.log(2);
console.log(3);
上述代码往队列里添加了3个消息,并依次执行,所以打印1 2 3
然后,将console.log(2)
包裹setTimeout
console.log(1);
setTimeout(function() {
console.log(2);
}, 0)
console.log(3);
setTimeout会延迟往队列里添加消息,0是最小延迟时间,但是它会等队列里其它消息执行完成以后在添加到队列。
所以,队列里先是同步任务,然后同步任务执行完成以后在添加setTimeout
任务。
因此,输出结果就是1 3 2
。
进一步添加setTimeout
console.log(1);
setTimeout(function() {
console.log(2);
}, 10)
setTimeout(function() {
console.log(3);
}, 0)
console.log(4);
到这里应该都理解了吧:首先队列是同步任务,队列执行完成以后添加剩余任务,第一个setTimeout延迟时间比较长,所以第二个setTimeout的任务会先添加到队列,所以输出:1 4 3 2
事件循环就是不断往执行队列里添加任务。
宏任务、微任务和EventLoop
上述事件循环中简述了同步任务和setTimeout执行过程,在ES6以后,添加了Promise方法、async和await关键字等支持异步的方式。
所以,我们通常会把异步任务分为宏任务和微任务。
宏任务:在主线程执行的任务,如setTimeout、setInterval等
微任务:一般是宏任务在线程中执行时产生的回调,如Promise.then
下面分析js执行过程
首先,js加载后直接执行的是同步任务,在同步任务执行过程中会产生宏任务(setTimeout)和微任务(then),将宏任务添加到任务队列尾部,微任务添加到宏任务前面,同步任务执行完成以后执行异步任务队列,这时先执行微任务,在执行宏任务,整个过程就是EventLoop。
根据上述过程可以分析一下下面的代码
console.log(1);
setTimeout(() => {
console.log(2)
})
new Promise((resolve) => {
console.log(3)
resolve()
}).then(() => {
console.log(4)
})
console.log(7);
- 同步打印1,setTimeout添加到任务队列,打印3,then添加到任务队列头部,然后打印5
- 执行队列,打印4,再打印2
关于复杂的案例网上有很多,无非各种嵌套,按照上述思路进行分析,可以解决任何关于Eventloop的问题。
相关资料
- MDN的 并发模型与事件循环
- MDN的《深入:微任务与Javascript运行时环境》
- 阮一峰老师的 什么是 Event Loop?
- 博主“_Draven”的 javascript的堆栈原理,写的比较形象。
- 博主“我的幸运7”的JavaScript函数的存储机制(堆)和执行原理(栈),函数执行过程比较详细。
- 博主“沐晓”的 JS事件循环(Event Loop) 分析的比较透彻
相关试题
- JS堆栈有什么区别?
- 堆和栈分别存储什么数据?
- 简述一下EventLoop的过程?
- 你知道宏任务和微任务吗?
- 知道函数在js引擎内的执行过程吗?
- 请写出控制台输出的内容
console.log(1) new Promise((resolve) => { console.log(2); setTimeout(() => { console.log(3) }); resolve(); console.log(4) throw new Error() console.log(5) }).then(() => { console.log(6) }).catch(() => { console.log(7) }) console.log(8)
- 请写出控制台输出的内容
console.log(1); setTimeout(() => { console.log(2); }); async function A() { console.log(3); await B(); console.log(4) } const B = () => { console.log(5) } A() new Promise((resolve) => { console.log(6); setTimeout(() => { console.log(7) }); resolve() }).then(() => { console.log(8) }).then(() => { console.log(9) }) new Promise().then(() => { console.log(10) }) console.log(11)