搞懂 Event Loop
本篇文章参考 Node.js 官方文档 javascript.info
什么是事件循环?
Event Loop 是实现异步的一种机制,由几个阶段(等待任务,执行任务等)循环组成,在 Node.js 和 Chrome 中不一样
Chrome EventLoop 两个阶段
宏任务(一会)和微任务(立刻)
按照中文语义,立刻任务立刻做,一会任务一会做,相同等级的任务按进入队列的顺序做(先进先出)
宏任务
引擎的一般算法:
- 当有任务时:
从最先进入的任务开始执行。 -
休眠直到出现任务,然后转到第 1 步。
image.png
微任务
ECMA 标准规定了一个内部队列 PromiseJobs,通常被称为“微任务队列(microtask queue)”(ES8 术语)
每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作。
更详细的事件循环图示如下(顺序是从上到下,即:首先是脚本,然后是微任务,渲染等):
image.png
在微任务之间没有 UI 或网络事件的处理:它们一个立即接一个地执行。所以,可以使用
queueMicrotask()来在保持环境状态一致的情况下,异步地执行一个函数。
总结
- 从 宏任务 队列(例如 “script”)中出队(dequeue)并执行最早的任务。
- 执行所有 微任务:
- 当微任务队列非空时:
- 出队(dequeue)并执行最早的微任务。
- 当微任务队列非空时:
- 如果有渲染,执行渲染。
- 如果宏任务队列为空,则休眠直到出现宏任务。
- 转到步骤 1。
API
setTimeout => 一会儿(宏任务)
setInterval => 一会儿(宏任务)
setImmediate => 一会儿(宏任务),优先级比 setTimeout 高
UI 渲染 => 一会儿(宏任务)
queueMicrotask() => 立刻(微任务)
.then(fn) => 立刻(微任务)
then 决定加入微任务队列,resolve 决定执行成功函数(第一个参数)不是失败函数(第二个参数)
await 转化成 Promise 再做,await下面的都属于回调函数,立刻(微任务)
new Promise(fn) 这里的fn相当于fn()
练一练
setTimeout(function () {
console.log(4);
}, 0);
new Promise(function (resolve) {
console.log(1);
resolve();
console.log(2);
}).then(function () {
console.log(5);
});
console.log(3);
1 2 3 5 4
async function async1() {
console.log(1);
await async2();
console.log(2);
}
async function async2() {
console.log(3)
}
async1();
new Promise(function (resolve) {
console.log(4);
resolve();
}).then(function () {
console.log(5);
});
1 3 4 2 5
Node.js EventLoop 各个阶段
因为 JavaScript 是单线程的,Event Loop 使 Node.js 能够将操作转移到系统内核中来执行非阻塞 I / O操作。
目前多数系统内核是多线程的,因此可以处理多个正在后台执行的操作。当某个操作完成,内核会告诉 Node.js,将相应的回调函数添加到轮询队列,然后执行回调函数,这个过程在 Node.js 中分为 6 个阶段。
当发送一个异步请求,请求时间为 1s 时,这段时间 JavaScript 会继续执行以下20行代码,轮询请求是否成功;当请求成功时,会把请求的内容返回给 JavaScript。由于JavaScript 是单线程的,因此肯定不是 JavaScript 做轮询,做轮询的是 C++ 。
当Node.js启动时,先初始化 Event Loop;执行 JavaScript,次过程可能调用异步API(计时器或 process.nextTick());然后处理事件循环。
下图显示了事件循环操作顺序的简化概述,每个方框是事件循环的阶段,每个阶段都有特定的操作。每个阶段都有一个任务队列,用于存放回调函数的地址;当 EventLoop 进入某个阶段时,它将执行该阶段特定的操作,然后执行该阶段任务队列中的回调函数;当任务队列用尽或回调函数全部执行完,EventLoop 将进入下一个阶段,依此类推。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
阶段描述
- timers: 此阶段执行
setTimeout()和安排setInterval()回调函数。
timers 的时间值表示在该时间值之后可以执行回调,而不是执行回调的确切时间。操作系统或其他回调函数的会延迟 timers 的回调,poll阶段控制 timers 阶段的执行时间。
- I/O callbacks: 执行推迟到下一个循环迭代的I / O回调。
- idle,prepare: 仅在内部使用
- poll: 某些时候 Node.js 将在此阶段阻塞,为了防止 poll 阶段占用太多时间,libuv(实现Node.js Event Loop 和所有异步操作的C库)对该阶段设定最大停留时间(具体时间取决于系统),EventLoop 大多数都是处在poll阶段。
如果 poll 队列不是空的,Event Loop 会依次执行队列里的回调函数,直到队列清空或到达 poll 阶段的时间上限(最大停留时间)。
如果 pull 队列为空,EventLoop 会检查计时器的时间是否到期,到期后 Eventloop 回到 timers 阶段,执行回调。
- check: 执行
setImmediate()
setImmediate()实际上是一个特殊的计时器,poll 队列空了,Event Loop 会结束 poll 阶段进入 check 阶段。 - close callbacks: 一些关闭回调,例如
socket.on('close', ...)
如果一个 socket 或者 handle 突然关闭(例如socket.destroy()),那么会进入这个阶段。否则将发出process.nextTick()
Node.js API
setTimeout() 和 setImmediate()
setTimeout()在 timers 阶段执行;setImmediate() 在 check 阶段执行
如果两者都是从主模块中调用的,则时序将受到进程性能的限制
setTimeout(() => {
console.log('fn1');
}, 0);
setImmediate(() => {
console.log('fn2');
});
setImmediate(() => {
console.log('fn2');
});
setTimeout(() => {
console.log('fn1');
}, 0);
执行时序不确定
因为初始化 Event Loop 是启动一个进程,需要一段时间;执行JavaScript 需要启动 V8 引擎,也需要时间,这两个进程的快慢是不确定的。
如果启动 Event Loop 很快,执行 JavaScript 时在 poll 阶段,这样先执行console.log('fn2'),再执行 console.log('fn1')
如果启动 Event Loop 很慢,JavaScript 先执行,这样会先把 console.log('fn1') 加入到timers 队列,再进入 poll 阶段,先执行console.log('fn1') 再执行 console.log('fn2')
process.nextTick()
在当前操作完成后执行,而不管 Eventloop 的当前阶段如何
作用
-
允许用户处理错误,清除所有不必要的资源,或者在事件循环继续之前重新尝试请求。
-
有时,有必要让回调在调用栈解开之后但事件循环继续之前运行
一个例子是匹配用户的期望。简单的例子:
const server = net.createServer();
server.on('connection', (conn) => { });
server.listen(8080);
server.on('listening', () => { });
练一练
setTimeout(() => {
setTimeout(fn1,0)
setImmediate(fn2)
},1000)
fn2先执行,然后fn1执行
setTimeout(() => {
setTimeout(fn1,0)
setImmediate(fn2)
process.nextTick(fn3)
},1000)
fn3,fn2,fn1
setTimeout(() => {
setTimeout(()=>{
console.log('fn1')
process.nextTick(()=>{console.log('fn4')})
},20)
setImmediate(()=>{console.log('fn2')})
process.nextTick(()=>{console.log('fn3')})
},1000)
fn3,fn2,fn1,fn4
setTimeout(() => {
setTimeout(()=>{
console.log('fn1')
process.nextTick(()=>{console.log('fn4')})
},20)
setImmediate(()=>{
console.log('fn2')
process.nextTick(()=>{console.log('fn5')})
})
process.nextTick(()=>{console.log('fn3')})
},1000)
fn3 fn2 fn5 fn1 fn4
setTimeout(() => {
setImmediate(() => {
console.log('1');
setTimeout(() => {console.log('2'),0})
});
setTimeout(() => {
console.log('3');
setImmediate(() => {console.log('4')})
});
},1000)
1 3 4 2或 1 3 2 4
setTimeout(() => {
setImmediate(() => {
console.log('1');
setTimeout(() => {console.log('2'),0})
});
setTimeout(() => {
console.log('3');
process.nextTick(()=>{console.log('5')})
setImmediate(() => {console.log('4')})
});
},1000)
1 3 5 4 2 或 1 3 5 2 4
setTimeout(() => {
setImmediate(() => {
console.log('1');
process.nextTick(()=>{console.log('5')});
setTimeout(() => {console.log('2'),0})
});
setTimeout(() => {
console.log('3');
setImmediate(() => {console.log('4')})
});
},1000)
1 5 3 4 2 或 1 5 3 2 4