浅谈nodejs事件循环(适合新手)事件循环介绍 事件循环是Node自身自身的一个执行模型,是一个典型的生产者/消费者模
事件循环介绍
事件循环是Node自身自身的一个执行模型,是一个典型的生产者/消费者模型。在事件循环中有一个或者多个观察者,在判断是否存在事件需要处理的过程就是观察者询问又要处理的事件。
下面是一个简易的tick流程图
事件tick阶段
在事件循环中,是一次完整的事件循环Tick分成很多个阶段
- 定时器(Timers):本阶段执行已经被setTimeout和setInterval的调度回调函数。
- 待定回调(PendingCallback):对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到ECONNREFUSED。
- idle,prepare:仅系统内部使用,process.nextTick。
- 轮询(Poll):检索新的I/O事件;执行与I/O相关的回调
- check检测:setTimeout/setInterva,setImmediate回调函数在这里执行。
- 关闭的回调函数:一些关闭的回调函数,如:socket的关闭事件。 注意:上述的阶段中都是存在优先级的,主要原因是因为事件循环对观察者的检测是有先后循序的。 idle观察者 > I/O观察者 > check观察者
宏任务和微任务
Node中的异步任务主要分为两种,宏任务和微任务,其中微任务的优先级高于宏任务,也就是说只有执行完微任务后才会执行下一个宏任务。特别注意:在第一次执行时,会先执行一个宏任务。 所以并不是说微任务一定永远比宏任务优先执行。 主要的宏任务和微任务在Node中存在有哪些: 宏任务:
- io任务
- setTimeout、setInterval
- setImmediate
微任务:
- promise的回调方法,如:promise.then,promise.catch,promise.finally
- 使用了await关键字后面的等待代码
- process.nextTick
事件循环原理
这篇文章讲得非常清晰,下面是文章中拿过来的原理和总结。
-
node 的初始化
- 初始化 node 环境。
- 执行输入代码。
- 执行 process.nextTick 回调。
- 执行 microtasks。
-
进入 event-loop
-
进入 timers 阶段
- 检查 timer 队列是否有到期的 timer 回调,如果有,将到期的 timer 回调按照 timerId 升序执行。
- 检查是否有 process.nextTick 任务,如果有,全部执行。
- 检查是否有microtask,如果有,全部执行。
- 退出该阶段。
-
进入IO callbacks阶段。
- 检查是否有 pending 的 I/O 回调。如果有,执行回调。如果没有,退出该阶段。
- 检查是否有 process.nextTick 任务,如果有,全部执行。
- 检查是否有microtask,如果有,全部执行。
- 退出该阶段。
-
进入 idle,prepare 阶段:
- 这两个阶段与我们编程关系不大,暂且按下不表。
-
进入 poll 阶段
-
首先检查是否存在尚未完成的回调,如果存在,那么分两种情况。
-
第一种情况:
- 如果有可用回调(可用回调包含到期的定时器还有一些IO事件等),执行所有可用回调。
- 检查是否有 process.nextTick 回调,如果有,全部执行。
- 检查是否有 microtaks,如果有,全部执行。
- 退出该阶段。
-
第二种情况:
- 如果没有可用回调。
- 检查是否有 immediate 回调,如果有,退出 poll 阶段。如果没有,阻塞在此阶段,等待新的事件通知。
-
-
如果不存在尚未完成的回调,退出poll阶段。
-
-
进入 check 阶段。
- 如果有immediate回调,则执行所有immediate回调。
- 检查是否有 process.nextTick 回调,如果有,全部执行。
- 检查是否有 microtaks,如果有,全部执行。
- 退出 check 阶段
-
进入 closing 阶段。
- 如果有immediate回调,则执行所有immediate回调。
- 检查是否有 process.nextTick 回调,如果有,全部执行。
- 检查是否有 microtaks,如果有,全部执行。
- 退出 closing 阶段
-
检查是否有活跃的 handles(定时器、IO等事件句柄)。
- 如果有,继续下一轮循环。
- 如果没有,结束事件循环,退出程序。
在事件循环的每一个子阶段退出之前都会按顺序执行如下过程:
- 检查是否有 process.nextTick 回调,如果有,全部执行。
- 检查是否有 microtaks,如果有,全部执行。
- 退出当前阶段。
-
可以直接理解为下面这样子(记住这个图可以轻松拿捏事件循环的笔试面试题,但是,面试官问原理的时候就要老老实实按照前面的原理解释,后面会有个例子可以参考理解这个图):
队列解释:
- main script 相当于main函数执行入口
- nextTicks process.nextTick()时加入的队列
- other microtask 微任务队列(promise)
- timers 定时器函数执行时的队列
- immediate(check阶段) setImmediate()时的队列
开练,经典面试题

解释:
- 代码依次执行,首先先定义了两个方法
async function async1 () {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2 () {
console.log('async2')
}
- 继续向下执行代码,此时这段代码会加入到main script队列中(相当于main方法)
console.log('script start')
- 继续向下执行代码,注意这里的定时器的时间是0,会立即加入到timer队列中
setTimeout(function () { console.log('setTimeout0') }, 0)
- 继续向下执行代码,这里的定时器为延迟300毫秒后才执行回调,此时这段代码不会立即加入到timer队列中,会被添加到红黑树的某个位置等待添加到timer队列中
setTimeout(function () {
console.log('setTimeout2')
}, 300)
- 继续向下执行代码,这里执行的是setImmediate,此时这段代码会添加到immediate队列
setImmediate(() => console.log('setImmediate'));
- 继续向下执行代码,此时这段代码会被添加nexttick队列
process.nextTick(() => console.log('nextTick1'));
- 继续向下执行代码,此时执行async1方法,直接将打印的async1 start添加到main script队列中
async1();
// console.log('async1 start') 添加到main script队列中
- 继续执行async1方法,此时执行到 await async2(),async2方法会被立即执行,将打印的async2加入到main 队列,注意:当执行完前面的内容后,await async2() 后面的方法会被添加到微队列中,也就是图中的other microtask 队列
await async2() // 执行这个方法,
// console.log('async2') 这里会被立即执行,将打印的async2加入到main 队列
// async1方法中 await async2()后面的console.log('async1 end')被添加到other microtask 队列
- 此时async1执行告一段落,继续向下执行代码,process.nextTick会被添加到nexttick队列中
process.nextTick(() => console.log('nextTick2'));
- 继续向下执行代码,Promise中的回调方法会立即执行(console.log('promise1')加入到main script队列),then的回调会被加入到微任务队列(console.log('promise3')加入到other microtask队列),执行完 resolve()后,立即执行console.log('promise2'),console.log('promise2')被添加到main script队列
new Promise(function (resolve) {
console.log('promise1')
resolve();
console.log('promise2')
}).then(function () {
console.log('promise3')
})
- 继续向下执行代码,执行最后一段代码,console.log('script end')会被添加到main script队列中
console.log('script end')
- 最后一次执行添加到这些队列中的所有任务得到最终结果
// script start
// async1 start
// async2
// promise1
// promise2
// script end
// nextTick1
// nextTick2
// async1 end
// promise3
// setTimeout0
// setImmediate
// setTimeout2
图解: 序号1-13为代码执行顺序,最终输出结果按照队列从上到下,从左到右依次执行得出结果。
转载自:https://juejin.cn/post/7376940200914649126