node事件循环
什么是事件循环?
尽管js是单线程的,但是只要一有可能,事件循环通过切断和系统内核之间的联系,来实允许node执行非阻
塞I/O。
因为大多数现代内核都是多现成的,他们能在后台处理执行多个操作。当其中一个操作完成后,内核就会通
知node以便相应的回调添加到poll队列中,该回调最终会被执行。我们稍后将在本文中详细阐述。
当node开始后,他会初始化事件循环,处理提供的脚本,这些脚本中可能有异步api,定时任务,或者是下个
轮询。然后开始执行事件循环。
下面这张图简单描述了事件循环的先后顺序。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
每个阶段都有一个先进先出的队列要执行,虽然每个阶段都有自己的特出之处,一般来讲,当事件循环进入
一个给定的阶段,他将会执行该阶段的任何操作。然后执行这个阶段队列中中的回调函数,直到该队列执行
完毕,或者达到了执行回调的最大次数。当队列并执行完,或者达到了回调最大限制。然后执行下一个阶段
,以此类推。
由于这些操作中的任何一个都可以调度更多的操作,并且轮询阶段中处理的新事件由内核排队。
轮询事件可以在处理轮询事件时排队。长时间运行的回调可以允许轮询阶段比定时器的阈值运行更长时间。
阶段概述
计时器(timers):此阶段执行由setTimeout()和setInterval()调度的回调。
挂起的回调(pending callbacks):执行推迟到下一个循环迭代的I/O回调。
闲着进程,准备(idle,prepare):只在内部使用。
poll:检索新的I/O事件;执行与I/O相关的回调(除了关闭回调、由定时器调度的回调和setImmediate()之
外,几乎所有回调都执行);node进程将在适当的时候阻塞这里。
check:这里调用setImmediate()回调。
关闭回调(close callbacks):一些关闭回调,例如socket.on('close',...)。
在每个循环之间node将会检查是否还有异步回调,或者定时任务,如果没有就关闭轮询。
定时器:
定时器指定执行所提供的回调的阈值,而不是希望执行回调的确切时间。定时器回调最早会在经过指定时间
量之后进行调度;然而,操作系统调度或其他回调的运行可能会延迟它们。
从技术上讲,轮询阶段控制计时器何时执行。
例如,假如说你打算100ms后执行一个回调,然后你的脚本读了一个文件花费了95毫秒。
const fs = require('fs');
function someAsyncOperation(callback) {
fs.readFile('./getWebPage.js', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
setImmediate(()=>{
console.log('do something after poll');
});
}, 0);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
console.log('read success');
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
console.log('10ms');
}
});
// const startCallback = Date.now();
// while(Date.now() - startCallback < 10){
// console.log('delay');
// }
当事件循环进入poll阶段,这里是一个空的队列,这是fs.readFile()还没有完成,它将等待剩余ms的数量,
直到达到最快定时器的阈值,95ms后fs.readFile()完成,并且他的需要10ms完成的回调函数,被添加进来并
且执行,当回调完成以后,队列里就没有其他回调,因此
事件循环将会查看最近的定时任务已经超过阈值,然后绕回到定时任务阶段,执行定时任务回调,在这个例
子中定时任务的延迟其实是105ms,也许会有疑问,如果把setTimeout执行时间设置为0,会怎么样呢?刚开
始线程开始执行,运行一个定时器,然后向下执行,等到整个执行完,检测定时器,发现定时器的阈值已经
到达(0),所以就执行了定时任务,所以先运行了setTimeout方法。如果定时任务是一个较大的数,这个数
比整个js运行完的事件要长,那么定时任务将在读取文件方法以后执行。以上经过测试。
挂起的回调。
这个阶段执行一些系统操作的回调,例如tcp连接错误,例如一个tcp套接字在想要进行连接的时候,接收到
ECONNERFUSED消息,一些lnix/unix系统想要等待去报告这个错误,他将会把这个错误放到挂起队列里执行。
(这里有个问题,假如在事件循环结束以后。这时候有一个错误,事件循环会从新开始吗?)
poll阶段:
这个阶段有两个主要的函数:
1:计算阻塞时间,poll forI/O。
2:执行poll队列里的事件。
当事件循环进入到poll阶段后如果没有定时任务,下面两种情况的一种将会发生。
1,如果poll队列非空,循环主线程将会一次同步的执行他们,直到队列被耗尽,或者达到系统规定的最大次
数(这里可能是cpu调度的限制)。
2,如果队列是空,还有两件事情会发生
2.1:如果脚本用setImmediate()做定时任务,事件循环将会结束poll阶段,到check阶段去执行定时任务
脚本。
2.2:如果没有setImmediate()的定时任务,事件循环将会等待回调倍添加到队列中然后立即执行他。
一旦poll队列为空,事件循环将会检查是否有定时任务到达阈值。如果有,事件循环将会回到timer阶段去执
行定时任务。
check阶段:
这个阶段允许你在poll阶段完成以后立马执行回调。如果poll阶段为空,脚本用setImmediate()排队,事件
循环检查这个阶段而不是等待。setImmediate()实际上是一个特殊的计时器,它在事件循环的独立阶段中运
行。它使用一个libuv API,用于在轮询阶段完成之后调度回调来执行。一般来讲当代码执行的时候事件循环
最终会听到poll阶段,他等待一个链接,请求,或者其他。可是如果回调函数使用的setImmediate(),一旦
poll阶段空闲下来就会运行这个回调。而不是在poll阶段等待。
关闭回调阶段:
如果一个链接或者句柄,被意外关闭了,close事件将会触发这个阶段。然而他通过process.nextTick()触发
。

浙公网安备 33010602011771号