事件循环
Node在启动时候会初始化event loop(事件循环),
众所周知JS是单线程事件循环方式,其中又有一些浏览器和Node各自支持的异步API,现在来说分为宏任务(macro task)和微任务(micro task)。微任务会在宏任务完成后执行,等全部微任务执行完毕再执行下一个宏任务
宏任务
浏览器 Node 整体代码(script) ✅ ✅ UI交互事件 ✅ ❌ I/O ✅ ✅ setTimeout ✅ ✅ setInterval ✅ ✅ setImmediate ❌ ✅ 微任务
浏览器 Node process.nextTick ❌ ✅ MutationObserver ✅ ❌ Promise.then catch finally ✅ ✅
Node
我们知道Node下定时器其实有很多选择,一般的setTimeout,但是如果精度要求比较高的时候,setTimeout不够用。因为setTimeout一般都是延后的,而且如果事件循环阻塞了,那么还会因阻塞而延迟,甚至完全不执行回调。
setTimeout的行为可以这么来看,首先setTimeout会生成定时器放入主循环(观察者),然后事件循环会定期查看主循环里的定时器是否到时间了,到时间了就取出执行回调函数。setInterval同理,只是不是一次性取出,而是可以多次取出,所以setInterval在事件循环卡住后,后面可能会短时间执行多次回调,由此而来。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
上图每个框都称为一个阶段,如timers阶段,poll阶段处理传入的连接数据等
Node事件循环顺序
- timers: 执行setTimeout和setInterval的回调
- pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调,比如
TCP socket
收到ECONNREFUSED
,类unix系统会想上报,会在这个队列里等待执行 - idle, prepare: 仅系统内部使用
- poll: 检索新的I/O事件;执行与I/O相关的回调(除了close回调、由计时器调度的回调和setImmediate()之外,几乎所有回调都在这里执行);节点也有可能在此阻塞
- check: setImmediate在这里执行
- close callbacks: 一些关闭的回调函数,如:socket.on('close', ...)
process.nextTick比较特别,是因为Node异步的设计理念而诞生,从技术上讲,process.nextTick()不是事件循环的一部分。相反,无论事件循环的当前阶段如何,nextTickQueue都将在当前操作完成后进行处理。这里,操作被定义为来自底层C/C++处理程序的转换,并处理需要执行的JavaScript。
任何时候在给定阶段调用process.nextTick()时,传递给process.nextTick()的所有回调都将在事件循环继续之前被解析。
process.nextTick适合用在什么地方?
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
this.emit('event');
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
MyEmitter构造函数想通知event事件监听,但是此时执行this.emit('event')
,事件还没绑定,这个时候调用会有问题。但是如果使用了process.nextTick
,那么可以正常执行
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
// use nextTick to emit the event once a handler is assigned
process.nextTick(() => {
this.emit('event');
});
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
宏任务顺序
直接上例子,node为v12.18.2
下本地亲测
- macroTask
- 整体代码、promise构造函数
- setTimeout、setInterval
- setImmediate
- microTask
- process.nextTick(虽然归到这里,但是process.nextTick并不算事件循环里的)
- promise.then回调
process.nextTick(()=>{
console.log(`process.nextTick 1`)
})
new Promise((resolve, reject) => {
console.log(`Promise exec`)
resolve()
}).then(() => {
console.log(`Promise then exec`)
})
process.nextTick(()=>{
console.log(`process.nextTick 2`)
})
setImmediate(() => {
console.log(`setImmediate 1`)
process.nextTick(() => {
console.log(`process.nextTick 3`)
})
})
setTimeout(() => {
console.log(`setTimeout 0`)
}, 0) // 注意在这里setTimeout最少为1,写为0则Node执行的时候会重置为1
setInterval(() => {
console.log(`setInterval 0`)
}, 0) // 同理,也会被重置为1
setImmediate(() => {
console.log(`setImmediate 2`)
})
console.log(`script end`)
输出结果如下,自己试注意无限循环
Promise exec
script end
process.nextTick 1
process.nextTick 2
Promise then exec
setTimeout 0
setInterval 0
setImmediate 1
process.nextTick 3
setImmediate 2
setInterval 0
setInterval 0
……
- 首先扫到process.nextTick、setImmediate的时候依次顺序加入
microTask
和macroTask
,来到new Promise
这里,Promise的构造函数是同步执行的,输出Promise exec
返回,then加入microTask
- 继续扫setTimeout同理加入
macroTask
,到最后console先输出。之后按照FIFO的形式清空microTask
,输出process.nextTick 1
,process.nextTick 2
,注意这里microTask里process.nextTick
优先级高,所以会先输出promise.nextTick
的,之后再输出Promise.then
的,Promise.then
执行完毕后microTask
已全部清空 - 开始执行下一个
macroTask
,同样事件循环里macroTask
是有优先级的,按照上面所述,首先是timers队列的先执行,所以是setTimeout 0
和setInterval 0
先输出,接着到poll I/O里的,没东西。然后是check阶段,执行setImmediate,输出setImmediate 1
。此时又遇到process.nextTick
,所以再次加入microTask
。 - 这时
macroTask
和microTask
都有任务,先清空microTask
再去执行下一个macroTask
的任务,所以先输出process.nextTick 3
,之后才是setImmediate 2
,之后就是microTask里的setInterval间隔1ms不停输出了
浏览器
浏览器的实现跟Node不一致,所以可能结果有前后区别。
async function async1() {
console.log("a");
const res = await async2();
console.log("b");
}
async function async2() {
console.log("c");
return 2;
}
console.log("d");
setTimeout(() => {
console.log("e");
}, 0);
async1().then(res => {
console.log("f")
})
new Promise((resolve) => {
console.log("g");
resolve();
}).then(() => {
console.log("h");
});
console.log("i");
/**
* 输出结果:d a c g i b h f e
*/
- 扫代码输出12行console的
d
,然后遇到14行setTimeout推入macroTask
(【macroTask有setTimeout,microTask为空】) - 之后18行
async1
执行输出第2行的a
,执行asycn2输出第8行的c
,同时第9行async2
返回2,但是被第3行的await阻塞,后面的赋值和console.log等等被阻塞,等待执行(【macroTask有setTimeout,microTask有第3行await阻塞的】) - 继续到22行Promise构造函数,输出
g
后25行Promise.then
加入microTask
(【macroTask有setTimeout,microTask有第3行await阻塞的和25行的Promise.then】) - 继续扫script输出
i
- 要清空microTask,找到最先加入的,就是第3行的await阻塞的,继续执行第三行res赋值为2,然后输出
b
,async1执行完毕,到18行的Promise.then
,加入microTask
(【macroTask有setTimeout,microTask有25行的Promise.then
和18行的Promise.then
】) - 取microTask的25行的
Promise.then
执行,输出h
,再取18行的Promise.then
,输出f
(【macroTask有setTimeout) - microTask清空,继续macroTask执行,取setTimeout执行,输出
e
,全部结束
参考
