前言
好久没写博客了,因为赶着学业和打码..........鸽了好久抱歉抱歉orz
如果你比较有能力,我完全推荐你直接去看官方文档
https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/
这里是在官方文档的基础上做的一个自己的解析与理解。
正文
事件循环操作顺序:
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
本文着重介绍timers(定时器) poll(轮询) check()三个阶段
part 1.定时器阶段(timers)
取自官方:
计时器指定 可以执行所提供回调 的 阈值,而不是用户希望其执行的确切时间。在指定的一段时间间隔后, 计时器回调将被尽可能早地运
行。但是,操作系统调度或其它正在运行的回调可能会延迟它们。注意:轮询 阶段 控制何时定时器执行。
例如,假设您调度了一个在 100 毫秒后超时的定时器,然后您的脚本开始异步读取会耗费 95 毫秒的文件:
const fs = require('fs'); function someAsyncOperation(callback) { // Assume this takes 95ms to complete fs.readFile('/path/to/file', callback); } const timeoutScheduled = Date.now(); setTimeout(() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms have passed since I was scheduled`); }, 100); // do someAsyncOperation which takes 95 ms to complete someAsyncOperation(() => { const startCallback = Date.now(); // do something that will take 10ms... while (Date.now() - startCallback < 10) { // do nothing } });
当事件循环进入 轮询 阶段时,它有一个空队列(此时
fs.readFile()
尚未完成),因此它将等待剩下的毫秒数,直到达到最快的一个计时器阈值为止。当它等待 95 毫秒过后时,fs.readFile()
完成读取文件,它的那个需要 10 毫秒才能完成的回调,将被添加到 轮询 队列中并执>行。当回调完成时,队列中不再有回调,因此事件循环机制将查看最快到达阈值的计时器,然后将回到 计时器 阶段,以执行定时器的回调。在本示例中,您将看到调度计时器到它的回调被执行之间的总延迟将为 105 毫秒。
注意:为了防止 轮询 阶段饿死事件循环,libuv(实现 Node.js 事件循环和平台的所有异步行为的 C 函数库),在停止轮询以获得更多事件之前,还有一个硬性最大值(依赖于系统)。
注意到这句话:
当事件循环进入 轮询 阶段时,它有一个空队列(此时
fs.readFile()
尚未完成),因此它将等待剩下的毫秒数,直到达到最快的一个计时器阈值为止。
意味着在轮询阶段有一个阻塞的过程(同步等待意味着阻塞)
这个阻塞并非一成不变,会在轮询队列由 空 - >回调入队(这里是fs.readFile()) 时清空队列,执行回调,此时可能会延迟即将到来的timer函数
大致过程->
[ ] 0ms
->计算阻塞时间
->等待
[ fs.readFileCallback ] 95ms 这是一个意外的事件(可能不会发生)
->readFileCallback to end 105ms
->检测到可以执行time回调了 虽然已经延迟5ms
->回到timer阶段
->执行time回调
略过了pending callback阶段,因为不影响对事件队列的理解
part 2.轮询阶段(poll)
轮询 阶段有两个重要的功能:
计算应该阻塞和轮询 I/O 的时间。[1]
然后,处理 轮询 队列里的事件。
当事件循环进入 轮询 阶段且 没有被调度的计时器时 ,将发生以下两种情况之一:如果 轮询 队列 不是空的 ,事件循环将循环访问回调队列并同步执行它们,直到队列已用尽,或者达到了与系统相关的硬性限制。[2]
如果 轮询 队列 是空的 ,还有两件事发生:
如果脚本被 setImmediate() 调度,则事件循环将结束 轮询 阶段,并继续 检查 阶段以执行那些被调度的脚本。
如果脚本 未被 setImmediate()调度,则事件循环将等待回调被添加到队列中,然后立即执行。
一旦 轮询 队列为空,事件循环将检查 已达到时间阈值的计时器。如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行这些计时器的回调。
检查阶段
此阶段允许人员在轮询阶段完成后立即执行回调。如果轮询阶段变为空闲状态,并且脚本使用 setImmediate() 后被排列在队列中,则事件循环可能继续到 检查 阶段而不是等待。setImmediate() 实际上是一个在事件循环的单独阶段运行的特殊计时器。它使用一个 libuv API 来安排回调在 轮询 阶段完成后执行。
通常,在执行代码时,事件循环最终会命中轮询阶段,在那等待传入连接、请求等。但是,如果回调已使用 setImmediate()调度过,并且轮询阶段变为空闲状态,则它将结束此阶段,并继续到检查阶段而不是继续等待轮询事件。
注释[1]猜测和timer阶段的硬性最大值有关,此时要算出本次轮询阶段一共花费多长时间,总不能一直卡着吧!
注释[2]这里的轮询队列说的应该就是事件队列,不过除开官方文章,很多描述事件循环的文章喜欢把它叫做事件队列,这是不是一种长期的误解?
个人简述此阶段:
[1]计算轮询时间T1
[2]处理轮询队列事件
[3]判断轮询队列情况
不空->尽可能在T1内执行队列中的事件
空->等待回调被添加中,然后立即执行
空->(特殊情况)在此期间,如果被setImmediate调度,则直接进入下一阶段。
part 3.检查阶段(check)
此阶段允许人员[1]在轮询阶段完成后立即执行回调。如果轮询阶段变为空闲状态,并且脚本使用 setImmediate() 后被排列在队列中,则事件循环可能继续到检查阶段而不是等待。
setImmediate() 实际上是一个在事件循环的单独阶段运行的特殊计时器。它使用一个 libuv API 来安排回调在 轮询 阶段完成后执行。
通常,在执行代码时,事件循环最终会命中轮询阶段,在那等待传入连接、请求等。但是,如果回调已使用 setImmediate()调度过,并且轮询阶段变为空闲状态,则它将结束此阶段,并继续到检查阶段而不是继续等待轮询事件。[2]
[1]人员?说实话这里不是很懂人员的含义是指程序员吗?但查看英文文档,这里的人员就是people.有些疑惑
[2]这里指的就是上文注解中:空->(特殊情况)在此期间,如果被setImmediate调度,则直接进入下一阶段。
说实话这里的check阶段是专门为 setImmediate准备的,setImmediate为何有这么大能耐?
part 4.关闭的回调函数阶段(close callbacks)
如果套接字或处理函数突然关闭(例如 socket.destroy()),则'close' 事件将在这个阶段发出。否则它将通过 process.nextTick() 发出。
当此阶段结束,node将回到timers进行循环,也就是完成了一轮事件循环、