事件轮询机制
事件执行顺序:
引入一下两个概念:
- 宏任务(Macrotasks):就是参与了浏览器事件循环的异步任务
宏任务有:setTimeout,setInterval - 微任务(Microtasks): 直接在 Javascript 引擎中的执行的,没有参与浏览器事件循环的任务。
微任务:promise的then,await后面的语句,process.nextTick
执行的顺序是**‘先同后异,先微后宏’**
事件轮询机制阶段:
- 定时器:本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
- 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
- idle, prepare:仅系统内部使用。
- 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
- 检测:setImmediate() 回调函数在这里执行。
- 关闭的回调函数:一些关闭的回调函数,如:socket.on(‘close’, …)。
定时器:本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
会在轮询阶段最后执行
待定回调:执行延迟到下一个循环迭代的 I/O 回调。
系统操作的回调
idle, prepare:仅系统内部使用。
轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
- 队列不为空:循环访问回调队列,并同步执行
- 队列为空:
- 有setImmidiate(cb),结束轮询,到达检查阶段执行setImmidiate的cb
- 无setImmidiate(cb),被加入队列,并同步执行事件。
无setImmidiate(cb)举例:
一般情况下是这样
一个的时候:
new Promise((resolve, reject) => { console.log(1); resolve(2)})
.then(data => console.log(data))
第一次进入轮询阶段,轮询队列为空 && 无setImmidiate,则then
会被加入到队列,console.log(1); resolve(1)
会被执行。then需要等待下次轮询。
到第二次进入轮询阶段,则队列里面有then
,则调用then的回调data=>console.log(data)
多个的时候:
new Promise((resolve, reject) => {
console.log(1)
resolve(2)
}).then(console.log)
new Promise((resolve, reject) => {
console.log(5)
resolve(6)
}).then(data => {
console.log(data)
new Promise((resolve, reject) => {
console.log(7)
resolve(8)
}).then(console.log)
new Promise((resolve, reject) => {
console.log(9)
resolve(10)
}).then(console.log)
})
new Promise((resolve, reject) => {
console.log(3)
resolve(4)
}).then(console.log)
console.log(0)
以上很可看出执行顺序是:
第一次轮询:
同步:1,5,3,0
进入微任务:2, 6, 7, 9, 4,
进入下一次轮询
进入微任务:8,10
有setImmidiate(cb)举例:
setImmediate(console.log, 0)
new Promise((resolve, reject) => {
console.log(11)
resolve(12)
}).then(console.log)
function asyncFn(cb) {
setImmediate(cb, 3)
}
asyncFn(data => {
new Promise((resolve, reject) => {
console.log(data)
resolve(6)
}).then(console.log)
new Promise((resolve, reject) => {
console.log(7)
resolve(8)
}).then(console.log)
})
setImmediate(console.log, 4)
new Promise((resolve, reject) => {
console.log(13)
resolve(14)
}).then(console.log)
console.log(5)
同步:11,13,5
异步:
进入轮询阶段
队列不为空,执行回调:12,14
队列为空:执行setImmediate
结束轮询
检查阶段:0
进入轮询阶段
队列为空
结束轮询
检查阶段:3,7
进入轮询阶段
队列不为空:6,8
轮询为空:执行setImmediate
结束轮询
检查阶段:4
这里跟promise的区别就是setImmediate会被轮询任务卡住,只有不为空的情况下才会被执行,其顺序是在每层轮询后才会被执行。
检测:setImmediate() 回调函数在这里执行。
执行回调
关闭的回调函数:一些关闭的回调函数,如:socket.on(‘close’, …)。
如果套接字或处理函数突然关闭(例如 socket.destroy()),则’close’ 事件将在这个阶段发出。否则它将通过 process.nextTick() 发出。
关注的问题:
process.nextTick() vs setImmediate() vs setTimeout()
从功能上来讲,其实setImmidiate的效果是下一次轮询前的执行,即nextTick。而process.nextTick的效果是在轮询阶段执行,刚好是Immediate的意思。这两者的名字应该是调转过来才对。
setTimeout: 最后执行
异步中任务顺序:
setImmediate(console.log, 0)
// 关注这两个
setTimeout(console.log, 0, 999)
setTimeout(console.log, 10, 1000)
new Promise((resolve, reject) => {
console.log(11)
resolve(12)
}).then(console.log)
function asyncFn(cb) {
setImmediate(cb, 3)
}
asyncFn(data => {
new Promise((resolve, reject) => {
console.log(data)
process.nextTick(console.log, 119)
resolve(6)
}).then(console.log)
new Promise((resolve, reject) => {
console.log(7)
process.nextTick(console.log, 229)
resolve(8)
}).then(console.log)
})
setImmediate(console.log, 4)
new Promise((resolve, reject) => {
console.log(13)
resolve(14)
}).then(console.log)
/*
11
13
12
14
999
0
3
7
119
229
6
8
4
1000
*/
setTimeout是看其阈值情况而定,
如果小于其阈值,比如为0,是在一次轮询后立即执行
promise.then = await的下一条语句 > process.nextTick > setTimeout > setImmediate
如果大于其阈值,比如设置300,则是多次轮询后,最后执行
promise.then = await的下一条语句 > process.nextTick > setImmediate > ...多轮 > setTimeout
以上就是为什么会在代码中经常看到使用setTimeout(fm, 0)
或者setTimeout(fm, 300)
的原因。
建议:
-
开发人员在所有情况下都使用 setImmediate(),因为它更容易理解
-
使用 setImmediate() 相对于setTimeout() 的主要优势是,如果setImmediate()是在 I/O 周期内被调度的,那它将会在其中任何的定时器之前执行,跟这里存在多少个定时器无关
我要注意什么事情?
如果运行以下不在 I/O 周期(即主模块)内的脚本,则执行两个计时器的顺序是非确定性的,因为它受进程性能的约束:
I/O 循环内调用,setImmediate 总是被优先调用:
比如:
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
为什么要使用 process.nextTick()?
有两个主要原因:
-
允许用户处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。
-
有时有让回调在栈展开后,但在事件循环继续之前运行的必要。
例子1:
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
// 执行构造函数,出发event事件但监听的代码还没执行到,触发的事件并未被监听到,下面的代码不会被执行。
// this.emit('event');
// 捕获到错误,并加入事件队列放到下次轮询中调用
process.nextTick(() => {
this.emit('event');
})
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
例子2:
const server = net.createServer();
server.on('connection', (conn) => { });
server.listen(8080);
server.on('listening', () => { });
这个例子效果同上,是我们常用常见的写法。其实一般我们会把.on
写在事件方式前,提前进行监听,这样才能捕捉到事件
参考:https://www.bookstack.cn/read/nodejs-guide/590a0c76ccf949ba.md#gxvmd
最后附上规律方便大家解题,一般面试题都会问到
事件循环机制规律:
- 同步,promise内,await后的函数
- 异步
- 微任务 promise、await、process.nextTick、setImmediate
优先级:promise.then = await的下一条语句 > process.nextTick > setImmediate > setTimeout - 宏任务 setTimeout setInterval
- 微任务 promise、await、process.nextTick、setImmediate