Randolf's blog

Event Loop

2019-04-25
javascript
6分钟
1096字

JavaScript 是单线程,不过 HTML5 提出新标准,允许 JavaScript 创建多个子线程,子线程还是受主线程控制。

由于主线程需要监听并多次调用子线程,就形成了 Event Loop。这么多个任务需要进入主线程,自然要排队执行,所以 JavaScript 除了主线程(负责同步任务),还需要一个任务队列(负责异步任务)。

主线程和任务队列

我们先了解一下主线程和任务队列。

  • 主线程
    在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务,这也称之为同步任务。

  • 任务队列
    进入任务队列的任务,不进入主线程,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行,这也称之为异步任务。

主线程和任务队列的示意图:

default

示意图分为两部分,左边主线程和右边任务队列,主线程负责渲染页面并监听任务队列,任务队列负责监听各种事件等候进入主线程。其中,任务队列可以放置异步任务事件和定时事件。

Event Loop

接下来了解一下 Event Loop。

维基对 Event Loop 的定义是:

In computer science, the event loop, message dispatcher, message loop, message pump, or run loop is a programming construct that waits for and dispatches events or messages in a program.

由此可见,Event Loop 是一种等待和分派事件的编程构造或程序消息,它在浏览器和 Nodejs 里的表现有所不同。

浏览器中的 Event Loop 示意图:

default

其中,任务队列包含两个定时事件:setTimeout 和 setInterval。

Nodejs 的 Event Loop 示意图:

default

下图显示 Event Loop 操作顺序:

1
┌───────────────────────────┐
2
┌─>│ timers │
3
│ └─────────────┬─────────────┘
4
│ ┌─────────────┴─────────────┐
5
│ │ pending callbacks │
6
│ └─────────────┬─────────────┘
7
│ ┌─────────────┴─────────────┐
8
│ │ idle, prepare │
9
│ └─────────────┬─────────────┘ ┌───────────────┐
10
│ ┌─────────────┴─────────────┐ │ incoming: │
11
│ │ poll │<─────┤ connections, │
12
│ └─────────────┬─────────────┘ │ data, etc. │
13
│ ┌─────────────┴─────────────┐ └───────────────┘
14
│ │ check │
15
│ └─────────────┬─────────────┘
3 collapsed lines
16
│ ┌─────────────┴─────────────┐
17
└──┤ close callbacks │
18
└───────────────────────────┘

其中,任务队列包含四个定时事件:setTimeout、setInterval、setImmediate 和 process.nextTick。

  1. Event Loop 里有个 poll 阶段,Node 很多 API 都是基于事件订阅完成的,这些 API 的回调应该都在 poll 阶段完成。
  2. 当 poll 阶段的回调执行完,setImmediate 具有最高优先级,只要 poll 队列为空,无论是否有 timers 达到下限时间,setImmediate 的回调都先执行。
  3. process.nextTick 可以理解成一个微任务。也就是说,它其实不属于 Event Loop 的一部分。不管在什么地方调用,他们都会在其所处的 Event Loop 最后,Event Loop 进入下一个循环的阶段前执行。

下面看几个例子来理解 Event Loop:

1
setImmediate(function A() {
2
console.log(1);
3
setImmediate(function B() {
4
console.log(2);
5
});
6
});
7
8
setTimeout(function timeout() {
9
console.log("TIMEOUT FIRED");
10
}, 0);
11
12
// 1, TIMEOUT FIRED, 2

这个例子中:

  1. 执行宏任务代码:1 TIMEOUT FIRED(这两个顺序可能交换)
  2. 遇到的新的宏任务代码并执行:2
1
process.nextTick(function A() {
2
console.log(1);
3
process.nextTick(function B() {
4
console.log(2);
5
});
6
});
7
8
setTimeout(function timeout() {
9
console.log("TIMEOUT FIRED");
10
}, 0);
11
// 1, 2, TIMEOUT FIRED

这个例子说明如果有多个 process.nextTick 语句(不管它们是否嵌套),将全部在当前”执行栈”执行:

  1. 先执行微任务代码:1
  2. 遇到新的微任务代码并执行:2
  3. 再执行剩下的宏任务代码:TIMEOUT FIRED

再来看两个稍难的例子:

1
setTimeout(() => {
2
console.log("timeout0");
3
process.nextTick(() => {
4
console.log("nextTick1");
5
process.nextTick(() => {
6
console.log("nextTick2");
7
});
8
});
9
process.nextTick(() => {
10
console.log("nextTick3");
11
});
12
console.log("sync");
13
setTimeout(() => {
14
console.log("timeout2");
15
}, 0);
2 collapsed lines
16
}, 0);
17
// timeout0, sync, nextTick1, nextTick3, nextTick2, timeout2

这个例子中:

  1. 没有全局代码,直接执行宏任务代码:timeout0 sync
  2. 接着执行微任务代码:nextTick1 nextTick3
  3. 遇到一个新的微任务并执行:nextTick2
  4. 最后执行剩下的宏任务代码:timeout2
1
console.log(1);
2
3
setTimeout(() => {
4
console.log(2);
5
Promise.resolve().then(() => {
6
console.log(3);
7
});
8
});
9
10
new Promise((resolve, reject) => {
11
console.log(4);
12
resolve(5);
13
}).then(data => {
14
console.log(data);
15
});
8 collapsed lines
16
17
setTimeout(() => {
18
console.log(6);
19
});
20
21
console.log(7);
22
23
// 1, 4, 7, 5, 2, 3, 6

这个例子中:

  1. 这个例子先执行全局代码:1 4 7
  2. 然后执行微任务代码:5
  3. 接着执行宏任务代码:2
  4. 遇到一个新的微任务并执行:3
  5. 最后执行剩下的宏任务代码:6

参考:

JavaScript 运行机制详解:再谈 Event Loop
由 setTimeout 和 setImmediate 执行顺序的随机性窥探 Node 的事件循环机制
The Node.js Event Loop, Timers, and process.nextTick()
Event loops
带你彻底弄懂 Event Loop

本文标题:Event Loop
文章作者:Randolf Zhang
发布时间:2019-04-25
Copyright 2025
站点地图