JavaScript的堆、栈、队列、事件循环机制

11/21/2020 JavaScriptPromiseES6

本文部分参考至:

脚本之家: JS 异步宏队列与微队列原理区别详解 (opens new window)

博客园:一只番茄仔 =>理解 JavaScript 中的堆和栈 (opens new window)

阮一峰老师:JavaScript 运行机制详解:再谈 Event Loop (opens new window)
阮一峰老师:Stack 的三种含义 (opens new window)

# 文章目录

在开始本文之间我们先来认识几个单词,没办法,笔者英语比较差:

  • heap [hiːp] 堆
  • stack [stæk] 栈
  • task queue 任务队列

此外 这里分享一个事件循环可视化的完整:事件循环可视化 (opens new window)

# 一、何为堆 何为栈?

堆 是堆内存的简称。栈 是栈内存的简称。

说到堆栈,我们讲的就是内存的使用和分配了,没有寄存器的事,也没有硬盘的事。
各种语言在处理堆栈的原理上都大同小异。堆是动态分配内存,内存大小不一,也不会自动释放。栈是自动分配相对固定大小的内存空间,并由系统自动释放。

javascript 的基本类型就 5 种:Undefined、Null、Boolean、Number 和 String,它们都是直接按值存储在栈中的,每种类型的数据占用的内存空间的大小是确定的,并由系统自动分配和自动释放。这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。

javascript 中其他类型的数据被称为引用类型的数据 : 如对象(Object)、数组(Array)、函数(Function) …,它们是通过拷贝和 new 出来的,这样的数据存储于堆中。其实,说存储于堆中,也不太准确,因为,引用类型的数据的地址指针是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据。

说来也是形象,栈,线性结构, 栈后进先出 ,便于管理。

# 二、Stack 的三种含义

学习编程的时候,经常会看到 stack 这个词,它的中文名字叫做"栈"。

理解这个概念,对于理解程序的运行至关重要。容易混淆的是,这个词其实有几种含义,适用于不同的场合,必须加以区分。

# 2.1 含义一:数据结构

stack的第一种含义是一组数据的存放方式,特点为 LIFO,即 后进先出(Last in, first out) 。
在这里插入图片描述
在这种数据结构中,数据像积木那样一层层堆起来,后面加入的数据就放在最上层。使用的时候,最上层的数据第一个被用掉,这就叫做"后进先出"。

与这种结构配套的,是一些特定的方法,主要为下面这些。

push:在最顶层加入数据。
pop:返回并移除最顶层的数据。
top:返回最顶层数据的值,但不移除它。
isempty:返回一个布尔值,表示当前stack是否为空栈。
1
2
3
4

# 2.2 内存区域

stack 的第二种含义是存放数据的一种内存区域。程序运行的时候,需要内存空间存放数据。一般来说,系统会划分出两种不同的内存空间:一种叫做 stack(栈),另一种叫做 heap(堆)。
在这里插入图片描述在这里插入图片描述
它们的主要区别是:stack 是有结构的,每个区块按照一定次序存放,可以明确知道每个区块的大小;heap 是没有结构的,数据可以任意存放。因此,stack 的寻址速度要快于 heap。

其他的区别还有,一般来说,每个线程分配一个 stack,每个进程分配一个 heap,也就是说,stack 是线程独占的,heap 是线程共用的。此外,stack 创建的时候,大小是确定的,数据超过这个大小,就发生 stack overflow 错误,而 heap 的大小是不确定的,需要的话可以不断增加。

根据上面这些区别,数据存放的规则是:只要是局部的、占用空间确定的数据,一般都存放在 stack 里面,否则就放在 heap 里面。请看下面这段代码(来源)。

public void Method1()
{
    int i=4;

    int y=2;

    class1 cls1 = new class1();
}
1
2
3
4
5
6
7
8

上面代码的 Method1 方法,共包含了三个变量:i, y 和 cls1。其中,i 和 y 的值是整数,内存占用空间是确定的,而且是局部变量,只用在 Method1 区块之内,不会用于区块之外。cls1 也是局部变量,但是类型为指针变量,指向一个对象的实例。指针变量占用的大小是确定的,但是对象实例以目前的信息无法确知所占用的内存空间大小。

这三个变量和一个对象实例在内存中的存放方式如下。
在这里插入图片描述
从上图可以看到,i、y 和 cls1 都存放在 stack,因为它们占用内存空间都是确定的,而且本身也属于局部变量。但是,cls1 指向的对象实例存放在 heap,因为它的大小不确定。作为一条规则可以记住,所有的对象都存放在 heap。

接下来的问题是,当 Method1 方法运行结束,会发生什么事?

回答是整个 stack 被清空,i、y 和 cls1 这三个变量消失,因为它们是局部变量,区块一旦运行结束,就没必要再存在了。而 heap 之中的那个对象实例继续存在,直到系统的垃圾清理机制(garbage collector)将这块内存回收。因此,一般来说,内存泄漏都发生在 heap,即某些内存空间不再被使用了,却因为种种原因,没有被系统回收。

# 三、队列

JS 中用来存储待执行回调函数的队列包含 2 个不同特定的队列

  • 宏列队 macrotask queue:用来保存待执行的宏任务(回调),比如:定时器回调、DOM 事件回调、ajax 回调
  • 微列队 microtask queue:用来保存待执行的微任务(回调),比如:promise 的回调、MutationObserver 、queueMiscrotask()的回调

JS 执行时会区别这 2 个队列

JS 引擎首先必须先执行所有的初始化同步任务代码
每次准备取出第一个宏任务执行前, 都要将所有的微任务一个一个取出来执行,也就是优先级比宏任务高,且与微任务所处的代码位置无关

队列 是一个先进先出 的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

# 四、事件循环机制 Event Loop

在这里插入图片描述

# 五、案例

# 3.1 AJAX 异步案例

执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。请看下面这个例子。

var req = new XMLHttpRequest();
req.open("GET", url);
req.onload = function() {};
req.onerror = function() {};
req.send();
1
2
3
4
5

上面代码中的 req.send 方法是 Ajax 操作向服务器发送数据,它是一个异步任务,意味着只有当前脚本的所有代码执行完,系统才会去读取"任务队列"。所以,它与下面的写法等价。

var req = new XMLHttpRequest();
req.open("GET", url);
req.send();
req.onload = function() {};
req.onerror = function() {};
1
2
3
4
5

也就是说,指定回调函数的部分(onload 和 onerror),在 send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取"任务队列"。

# 3.2 定时器异步案例

setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。

console.log(1);
setTimeout(function() {
  console.log(2);
}, 1000);
console.log(3);
1
2
3
4
5

上面代码的执行结果是 1,3,2,因为 setTimeout()将第二行推迟到 1000 毫秒之后执行。

如果将 setTimeout()的第二个参数设为 0,就表示当前代码执行完(执行栈清空)以后,立即执行(0 毫秒间隔)指定的回调函数。

setTimeout(function() {
  console.log(1);
}, 0);
console.log(2);
1
2
3
4

上面代码的执行结果总是 2,1,因为只有在执行完第二行以后,系统才会去执行"任务队列"中的回调函数。

总之,它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。

HTML5 标准规定了 setTimeout()的第二个参数的最小值(最短间隔),不得低于 4 毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为 10 毫秒。

需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在 setTimeout()指定的时间执行。

# 六、面试题

# 6.1 简单难度

第一题:

setTimeout(() => {
  console.log("timeout 1");
  Promise.resolve("3").then((value) => {
    console.log(value);
  });
}, 0);

setTimeout(() => {
  console.log("timeout 2");
}, 0);

Promise.resolve("1").then((value) => {
  console.log(value);
});

Promise.resolve("2").then((value) => {
  console.log(value);
});
console.log("0");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

output:

0
1
2
timeout 1
3
timeout 2
1
2
3
4
5
6

解释:HTML5 标准规定了 setTimeout()的第二个参数的最小值(最短间隔),不得低于 4 毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为 10 毫秒。

JS 引擎首先必须先执行所有的初始化同步任务代码,当主线程(执行栈)执行完成后,每次准备取出第一个宏任务执行前,都要将所有的微任务一个ー个取出来执行。

第二题:

setTimeout(() => {
  console.log(1);
}, 0);

new Promise((resolve) => {
  console.log(2);
  resolve();
})
  .then(() => {
    console.log(3);
  })
  .then(() => {
    console.log(4);
  });

console.log(5);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

output:

2 5 3 4 1
1

# 6.2 一般

第一题:

const first = () =>
  new Promise((resolve, reject) => {
    console.log(3);
    let p = new Promise((resolve, reject) => {
      console.log(7);
      setTimeout(() => {
        console.log(5);
        resolve(6);
      }, 0);
      resolve(1);
    });
    resolve(2);
    p.then((arg) => {
      console.log(arg);
    });
  });

first().then((arg) => {
  console.log(arg);
});
console.log(4);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

output:

3 7 4 1 2 5
1

# 6.3 困难

第一题:

setTimeout(() => {
  console.log("0");
}, 0);

new Promise((resolve, reject) => {
  console.log(1);
  resolve();
})
  .then(() => {
    console.log(2);
    new Promise((resolve, reject) => {
      console.log(3);
      resolve();
    })
      .then(() => {
        console.log(4);
      })
      .then(() => {
        console.log(5);
      });
  })
  .then(() => {
    console.log(6);
  });

new Promise((resolve, reject) => {
  console.log(7);
  resolve();
}).then(() => {
  console.log(8);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

output:

1 7 2 3 8 4 6 5 0
1
最后更新于: 2021年9月15日星期三晚上10点10分
Dawn
DDRKirby(ISQ)