秋招复习计划-JavaScript知识点9

  |  

前言

EventLoop事件循环和宏任务与微任务


37



EventLoop的相关概念

我们知道JS是一门单线程语言,既然是单线程,那么就有被阻塞的可能。EventLoop及事件循环,是解决JavaScript单线程运行阻塞的一种机制。但是在了解EventLoop机制之前我们得先掌握一些相关概念。

堆(Heap)

堆表示一大块非结构化的内存区域,对象,数据被存放在堆中

栈(Stack)

栈在JavaScript中又称为执行栈,调用栈,是一种后进先出的数据结构

JavaScript有一个主线程和调用栈(或执行栈call-stack),主线所有的任务都会被放到调用栈中等待主线程执行。当函数执行时,会被添加到栈的顶部,当执行栈执行完成后会从栈顶移除,直到栈内被清空。

举例说明:

1
2
3
4
5
6
7
8
9
function foo(b) {
var a = 10;
return a + b + 11;
}
function bar(x) {
var y = 3;
return foo(x * y);
}
console.log(bar(7)); // 42

当调用bar时,创建第一个帧,帧中包含bar的参数和局部变量,压入执行栈。当bar调用foo时,第二个帧被创建,并压入执行栈,位于第一个帧上方。但foo返回时,栈顶的帧被弹出,当bar返回时,栈空。

队列

在JS中,队列指任务队列(Task Queue),是一种先进先出的数据结构,在队尾添加新元素,从队头移除元素

基本的JavaScript事件循环

JavaScript是单线程,单线程就意味着所有任务都需要排队,前一个任务结束,才会执行下一个。如果前一个任务耗时很长,后一个任务不可能继续等待。

于是在设计之初,JS就将任务分成了两种:同步任务和异步任务

同步任务:是调用立即得到结果的任务,同步任务在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务

异步任务:是调用无法立即得到结果,需要额外的操作才能得到预期的任务,异步任务不进入主线程、而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

基本的JavaScript事件循环的执行过程如图所示

基本的事件循环

简述过程如下:

  • 同步任务和异步任务分别进入不同的执行场所,同步任务进入主线程的执行栈,异步的进入Event Table并注册函数。
  • 当指定的事情完成时,Event Table会见这个函数移入Event Queue(任务队列)
  • 主线程执行执行栈中的任务,执行栈空后,回去Event Queue中读取对应的回调函数,进入主线程执行
  • 上述过程会不断重复,也就是常说的Event Loop

JS引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空。

1
2
3
4
5
6
7
8
9
let data = [];
$.ajax({
url: wwww.test.com,
data: data,
success:() => {
consoel.log('send success');
}
})
console.log('代码执行结束');

分析这段代码:

  • ajax进入Event Table,注册回调函数success
  • 执行console.log(‘代码执行结束’)
  • ajax事件完成,回调函数success进入Event Queue
  • 主线程从Event Queue读取回调函数success并执行

这就是ES5及其以前版本对于JS事件循环的处理。接下来重点介绍一些ES5中的异步函数,以及它们对事件循环的影响

setTimeout

setTimeout给人的第一个印象就是可以延时异步执行函数,它接收了两个参数,回调函数和延时时间,延时时间以微秒为单位,例

1
2
3
setTimeout(() => {
console.log('test');
}, 3000)

setTimeout的一个额外的作用就是用作倒计时器,但是,用setTimeout做计时器会有细微的误差

1
2
3
4
setTimeout(() => {
console.log('倒计时3s')
}, 3000);
sleep(4000);

这里我们用setTimeout做了一个倒计时3s的计时器,但是实际上我们运行之后需要4s以后才会输出结果。分析这个程序的运行过程:

  • setTimeout函数进入Event Table注册箭头函数
  • 主线程执行执行栈中的sleep函数。
  • 3s以后,计时事件完成,匿名函数进入Event Queue中,这是sleep函数仍在执行
  • 再1s以后,sleep函数执行完毕,执行栈空,主线程冲Event Queue中提出匿名函数执行

这是因为,setTimeout的计时到了以后并不是立即执行,而是将回调函数放入到Event Queue中。

因此setTimeout还有setTimeout(fn, 0)这种用法,它表示某个任务在主线程最早可得的空闲时间执行

setInterval

setInterval是循环的执行,意味着每隔指定的时间将注册的函数放入Event Queue,如果前面的任务耗时太多,同样需要等待。而且如果setInterval的回调函数fn执行时间超过了延时时间,那么就完全看不出来有时间间隔了

ES6的事件循环

说完了ES5及以前的事件循环,来说说ES6的事件循环。ES6的事件循环和之前最大的区别在于加入了宏任务和微任务的概念。

宏任务和微任务

我们首先来看个例子

1
2
3
4
5
6
7
8
9
10
console.log('script start');
setTimeout(function() {
console.log('time over');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');

执行结果

为什么promise1和promise2会在time over之前打印,这就是ES6宏任务和微任务导致的。

ES6将所有任务分为宏任务和微任务

宏任务(macrotask):

  • script全部代码
  • setTimeout
  • setInterval
  • setImmediate
  • I/O操作
  • UI Rendering

微任务(microtask):

  • Process.nextTick,这个是Node独有的
  • Promise
  • Object.observe,这目前已经被废弃了
  • MutationObserver

由此原先的执行顺序就发生了改变,这时一次事件循环的执行步骤如下:

  • 从macrotask queue中取出最早的任务
  • 在执行栈中执行第一步取出的任务:
    • 如果任务中存在microtask,将其压入到microtask queue中,直到执行完毕
    • 如果任务中存在macrotask,将其压入macrotask queue中,直到执行完毕
  • 执行栈空,设置为null
  • 从macrotask queue中删除执行过的macrotask
  • 取出microtask queue中的全部任务,放入执行栈中,注意这里的取是个持续的过程
  • 执行microtask:
    • 如果任务中存在macrotask,将其压入macrotask queue中
    • 如果任务中存在microtask,将其压入microtask queue中,由于是一直取,所以这里产生的microtask,会被立即执行
  • microtask queue空,返回第一步重复执行

宏微任务

了解完这些,我再回头去看一开始的示例代码,我们就知道他的执行过程了

1
2
3
4
5
6
7
8
9
10
console.log('script start');
setTimeout(function() {
console.log('time over');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
  • 首先,整段程序作为第一个宏任务进入主线程执行栈中执行,输出script start。
  • 遇到setTimeout,在Event Table中注册回调函数,由于延时设置的是0秒,所以注册后,回调函数会被立即放入macrotask queue中
  • 遇到Promise,执行resolve函数,then中两个函数被压入microtask queue中
  • 输出script end
  • 第一个宏任务执行完毕,执行栈空,取出microtask queue中的任务执行,输出promise1,再输出promise2
  • 第一次事件循环结束
  • 开始第二次事件循环,从macrotask queue中取出任务执行,也就是setTimeout的回调函数,输出time over

一个复杂的例子

通过之前对宏任务和微任务的分析,我们已经了解到了ES6版本的事件循环机制了,那么接下来,就让我们来看一个复杂的例子

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
32
33
34
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5');
})
});
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8');
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12');
})
});

让我们开始分析这个过程

首先第一轮事件循环:

  • 整体script作为第一个宏任务进入主线程执行栈执行遇到console.log,输出1
  • 遇到setTimeout,其回调函数被分发到macrotask queue内,我们将其记为setTimeout1
  • 遇到process.nextTick(),其回调函数被分发到microtask queue内,我们将其记为process1
  • 遇到Promise,new Promise直接执行,输出7。the被发送到microtask queue内,我们将其记为then1
  • 又遇到setTimeout,将其回调函数发送到macrotask queue内,我们将其记为setTimeout2

此时我们已经输出了1、7,而这时的macrotask queue和microtask queue队列内有

macrotask queue microtask queue
setTimeout1 process1
setTimeout2 then1
  • 第一个宏任务执行完毕,执行栈空,将microtask queue中的任务压入执行栈执行,注意队列是先进先出的
  • 执行process1,输出6
  • 执行then1,输出8

第一轮事件循环结束,目前我们输出了1,7,6,8。接下来执行第二轮事件循环:

  • 从macrotask queue中取出setTimeout1压入执行栈执行,输出2,这时队列里有
macrotask queue microtask queue
setTimeout1
  • 接下来,我们遇到了process.nextTick(),将其分发到microtask queue中,记为process2。
  • new Promise立即执行,输出4,then被分发到microtask queue中,记为then2,这时队列里
macrotask queue microtask queue
setTimeout2 process2
then2
  • 这是setTimeout1执行完毕,执行栈栈空,开始执行process2和then2这两个微任务
  • 输出3,5,第二轮事件循环结束

目前我们已经输出了1,7,6,8,2,4,3,5。之后是第三次事件循环:

  • 将setTimeout2压入执行栈执行,输出9
  • 将process.nextTick()方法到microtask queue中,记为process3
  • 执行new Promise,输出11
  • 将then分发到microtask queue中,记为then3,这时队列里
macrotask queue microtask queue
process2
then3
  • setTimeout2执行完毕,执行栈空,执行microtask queue中的任务
  • 输出10
  • 输出12

到现在我们进行了三次事件循环,所有的结果都已输出,输出结果为1 7 6 8 2 4 3 5 9 11 10 12。(这个在Node11下是正确的,而我在Node10下输出的结果是1 7 6 8 2 4 9 11 3 10 5 12,这可能跟Node的事件监听以来libuv有关,具体还是不清楚)

总结一下

js的异步

我们从最开头就说javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。

事件循环Event Loop

事件循环是js实现异步的一种方法,也是js的执行机制。

javascript的执行和运行

执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。

setImmediate

微任务和宏任务还有很多种类,比如setImmediate等等,执行都是有共同点的,有兴趣的同学可以自行了解。

最后的最后

  • javascript是一门单线程语言
  • Event Loop是javascript的执行机制

垃圾语言

文章目录
  1. 1. EventLoop的相关概念
    1. 1.1. 堆(Heap)
    2. 1.2. 栈(Stack)
    3. 1.3. 队列
  2. 2. 基本的JavaScript事件循环
    1. 2.1. setTimeout
    2. 2.2. setInterval
  3. 3. ES6的事件循环
    1. 3.1. 宏任务和微任务
    2. 3.2. 一个复杂的例子
  4. 4. 总结一下
    1. 4.0.1. js的异步
    2. 4.0.2. 事件循环Event Loop
    3. 4.0.3. javascript的执行和运行
    4. 4.0.4. setImmediate
    5. 4.0.5. 最后的最后
|