事件循环
事件循环
其中 libuv 引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
从上图中,大致看出 node 中的事件循环的顺序:
外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O 事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段(按照该顺序反复运行)...
- timers 阶段:这个阶段执行 timer(setTimeout、setInterval)的回调
- I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调
- idle, prepare 阶段:仅 node 内部使用
- poll 阶段:获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里
- check 阶段:执行 setImmediate() 的回调
- close callbacks 阶段:执行 socket 的 close 事件回调
注意:上面六个阶段都不包括 process.nextTick() (下文会介绍)
接下去我们详细介绍timers、poll、check这 3 个阶段,因为日常开发中的绝大部分异步任务都是在这 3 个阶段处理的。
timer
timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。 同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行。
poll
poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情
1.回到 timer 阶段执行回调
2.执行 I/O 回调
并且在进入该阶段时如果没有设定了 timer 的话,会发生以下两件事情
-
如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
-
如果 poll 队列为空时,会有两件事发生
- 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调
- 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去
当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。
check 阶段
setImmediate()的回调会被加入 check 队列中,从 event loop 的阶段图可以知道,check 阶段的执行顺序在 poll 阶段之后。
我们先来看个例子:
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
复制代码
- 一开始执行栈的同步任务(这属于宏任务)执行完毕后(依次打印出 start end,并将 2 个 timer 依次放入 timer 队列),会先去执行微任务(这点跟浏览器端的一样),所以打印出 promise3
- 然后进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise.then 回调放入 microtask 队列,同样的步骤执行 timer2,打印 timer2;这点跟浏览器端相差比较大,timers 阶段有几个 setTimeout/setInterval 都会依次执行,并不像浏览器端,每执行一个宏任务后就去执行一个微任务(关于 Node 与浏览器的 Event Loop 差异,下文还会详细介绍)
观察者
在Node中,事件主要来源于网络请求,文件I/O等,这些事件都有相应的观察者。
事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求等是事件的生产者,事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。
请求对象
JavaScript发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,被称为请求对象。也就是说回调函数并不是由开发者调用而是由请求对象进行调用。
- JS调用Node核心模块
- Node Core调用C++内建模块
- 内建模块通过libuv进行系统调用。此时会生成一个请求对象,JS层传入的参数和方法都包装在这个请求对象中,包括回调函数(被设在oncomplete属性上)
- 对象包装完成后,Windows平台会将对象推入线程池中等待执行。
执行回调
- 线程池中的I/O操作执行完毕后,会将获取到的结果存储在req->result属性上,然后通知IOCP(windos平台实现异步I/O的解决方案),告知当前对象操作已完成
- 此时会调用事件循环的I/O观察者,在每次Tick的执行中,他会调用ICOP相关的方法检测线程池中是否含有未执行完毕的请求。如果存在,会将请求对象加入到I/O观察者的队列中,然后将其当做事件处理。至此整个异步I/O操作到此结束
- 点赞
- 收藏
- 关注作者
评论(0)