JS基础-6|堆栈、队列、事件循环、宏任务、微任务、EventLoop

646 阅读7分钟

堆栈和队列涉及到计算机底层原理,没有计算机基础的更需要学习一下。
前几篇沉淀了变量、函数,但是他们在计算机是如何存储运行的,本篇沉淀的内容就是前几篇的知识点的底层分析。

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. 同步打印1,setTimeout添加到任务队列,打印3,then添加到任务队列头部,然后打印5
  2. 执行队列,打印4,再打印2

关于复杂的案例网上有很多,无非各种嵌套,按照上述思路进行分析,可以解决任何关于Eventloop的问题。

相关资料

相关试题

  • 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)