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()触发 

posted @ 2018-12-22 11:27  node-吉利  阅读(353)  评论(0)    收藏  举报