Node.js以异步I/O和事件驱动的特性著称,但异步I/O是怎么实现的呢?其中核心的一部分就是event loop,下文中内容基原本自于Node.js文档,有不许确地方请指出.javascript
event loop能让Node.js的I/O操做表现得无阻塞,尽管JavaScript是单线程的但经过尽量的将操做放到操做系统内核.java
因为如今大多数内核都是多线程的,它们能够在后台执行多个操做. 当这些操做完成时,内核通知Node.js应该把回调函数添加到poll队列被执行.咱们将在接下来的话题里详细讨论.node
当Node.js开始时,它将会初始化event loop,处理提供可能形成异步API调用,timers任务,或调用process.nextTick()
的脚本(或者将它放到[REPL][]中,这篇文章中将不会讨论),而后开始处理event loop.git
下面是一张event loop操做的简单概览图.github
┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
注意: 每个方框将被简称为一个event loop的阶段.npm
每个阶段都有一个回调函数的FIFO队列被执行.每个阶段都有本身特有的方式,一般even loop进入一个给定的阶段时,它将执行该阶段任何的特定操做,而后执行该阶段队列中的回调函数,直到执行完全部回调或执行了最大回调的次数.当队列中的回调已被执行完或者到达了限制次数,eventloop将会从下一个阶段开始依次执行.api
因为这些操做可能形成更多的操做,而且在poll阶段中产生的新事件被内核推入队列,因此poll事件能够被推入队列当有其它poll事件正在执行时.所以长时间执行回调能够容许poll阶段超过timers设定的时间.详细内容请看timers和poll章节.浏览器
ps: 我的理解-在轮询阶段一个回调执行可能会产生新的事件处理,这些新事件会被推入到轮询队列中,因此poll阶段能够一直执行回调,即便timers的回调已到时间应该被执行时.多线程
注意: Windows和Unix/Linux在实现时有一些细微的差别,但那都不是事儿.重点是: 实际上有7或8个步骤,Node.js实际上使用的是它们全部.异步
setTimeout()
和 setInterval()
产生的回调.setImmediate()
的回调.setImmediate()
回调.socket.on('close', ...)
.在每次event loop之间,Node.js会检查它是否正在等待任何异步I/O或计时器,若是没有就会彻底关闭.
一个定时器指定的是执行回调函数的阈值,而不是肯定的时间点.定时器的回调将在规定的时间事后运行;然而,操做系统调度或其余回调函数的运行可能会使执行回调延迟.
注意: 技术上,poll 阶段控制了timers被执行.
例如, 你要在100ms的延时后在回调函数而且执行一个耗时95ms的异步读取文本操做:
const fs = require('fs'); function someAsyncOperation(callback) { // Assume this takes 95ms to complete fs.readFile('/path/to/file', callback); } const timeoutScheduled = Date.now(); setTimeout(function() { 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(function() { const startCallback = Date.now(); // do something that will take 10ms... while (Date.now() - startCallback < 10) { // do nothing } }); // 输出: 105ms have passed since I was scheduled
当event loop进入poll阶段时,它是一个空的队列(fs.readFile()
尚未完成),因此它会等待数毫秒等待timers设定时间的到达.直到等待95 ms事后, fs.readFile()
完成文件读取而后它的回调函数会被添加至poll队列而后执行.当执行完成后队列中没有其余回调,因此event loop会查看定时器设定的时间已经到达而后回撤到timers阶段执行timers的回调函数.在例子里你会发现,从定时器被记录到执行回调函数耗时105ms.
注意: 为了防止poll阶段阻塞死event loop, [libuv]
(http://libuv.org/) (实现Node.js事件循环的C库和平台的全部异步行为)
也有一个固定最大值(系统依赖).
这个阶段执行一些系统操做的回调,例如TCP错误等类型.例如TCP socket 尝试链接时收到了ECONNREFUSED
,一些*nix系统想等待错误日志记录.这些都将在I/O callbacks阶段被推入队列执行.
poll 阶段有两个主要的功能:
当event loop进入poll阶段而且没有timers任务时会执行下面某一条操做:
若是poll队列为空,会执行下面某一条操作:
setImmediate()
执行,则event loop会结束 poll阶段,继续向下进入到check阶段执行setImmediate()
的脚本.setImmediate()
执行,event loop会等待回调函数被添加至队列,而后马上执行它们.一旦poll队列空了,event loop会检查timers是否有以知足条件的定时器,若是有一个以上知足执行条件的定时器,event loop将会撤回至timers阶段去执行定时器的回调函数.
这个阶段容许马上执行一个回调在poll阶段完成后.若是poll阶段已经执行完成或脚本已经使用setImmediate()
,event loop 可能就会继续到check阶段而不是等待.
setImmediate()
实际是在event loop 独立阶段运行的特殊定时器.它使用了libuv API来使回调函数在poll阶段后执行.
一般在代码执行时,event loop 最终会到达poll阶段,等待传入链接,请求等等.然而,若是有一个被setImmediate()
执行的回调,poll阶段会变得空闲,它将会结束并进入check阶段而不是等待新的poll事件.
若是一个socket或者操做被忽然关闭(例如.socket.destroy()
),这个close
事件将在这个阶段被触发.不然它将会经过process.nextTick()
被触发.
setImmediate()
vs setTimeout()
setImmediate
和 setTimeout()
是很类似的,可是它们的调用方式不一样致使了会有不一样的表现.
setImmediate()
会中断poll阶段,当即执行..setTimeout()
将在给定的毫秒后执行设定的脚本.timers的执行顺序会根据它们被调用的上下文而变化.若是两个都在主模块内被调用,则时序将受到进程的性能的限制(可能受机器上运行的其余应用程序的影响).
例如,咱们执行下面两个不在I/O周期内(主模块)的脚本,这两个timers的执行顺序是不肯定的,它受到进程性能的影响:
// timeout_vs_immediate.js setTimeout(function timeout() { console.log('timeout'); }, 0); setImmediate(function immediate() { console.log('immediate'); });
$ node timeout_vs_immediate.js timeout immediate $ node timeout_vs_immediate.js immediate timeout
然而,若是你把这两个调用放到I/O周期内,则immediate的回调总会被先执行:
// timeout_vs_immediate.js const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
$ node timeout_vs_immediate.js immediate timeout $ node timeout_vs_immediate.js immediate timeout
使用setImmediate()
比setTimeout()
的好处是setImmediate()
在I/O周期内老是比全部timers先执行,不管有多少timers存在.
process.nextTick()
process.nextTick()
你可能已经注意到process.nextTick()
没有在概览图中列出,尽管他是异步API的一部分.这是由于process.nextTick()
在技术上不是event loop的一部分.反而nextTickQueue
会在当前操做完成后会被执行,不管当前处于event loop的什么阶段.
再看看概览图,在给定的阶段你任什么时候候调用process.nextTick()
,经过process.nextTick()
指定的回调函数都会在event loop继续执行前被解析.这可能会形成一些很差的状况,由于它容许你经过递归调用process.nextTick()
而形成I/O阻塞死,由于它阻止了event loop到达poll阶段.
部分缘由是一个API应该是异步事件尽管它可能不是异步的.看看下面代码片断:
function apiCall(arg, callback) { if (typeof arg !== 'string') return process.nextTick(callback, new TypeError('argument should be string')); }
代码里对参数作了校验,若是不正确,它将会在回调函数中抛出错误.API最近更新,容许传递参数给 process.nextTick() ,process.nextTick()能够接受任何参数,回调函数被当作参数传递给回调函数后,你就没必要使用嵌套函数了.
咱们所作的就是将错误回传给用户当用户的其它代码执行后.经过使用process.nextTick()
咱们确保apiCall()
执行回调函数在用户的代码以后,在event loop运行的阶段以前.为了实现这一点,JS调用的堆栈被容许释放掉,而后马上执行提供的回调函数,回调容许用户递归的调用process.nextTick()
直到v8限制的调用堆栈最大值.
这种理念可能会致使一些潜在的问题.来看这段代码:
let bar; // this has an asynchronous signature, but calls callback synchronously function someAsyncApiCall(callback) { callback(); } // the callback is called before `someAsyncApiCall` completes. someAsyncApiCall(() => { // since someAsyncApiCall has completed, bar hasn't been assigned any value console.log('bar', bar); // undefined }); bar = 1;
用户定义了一个有异步标签的函数someAsyncApiCall()
,尽管他的操做是同步的.当它被调用的时候,提供的回调函数在event loop的同一阶段中被调用,由于someAsyncApiCall()
没有任何异步操做.因此回调函数尝试引用bar
尽管这个变量在做用域没有值,由于代码尚未执行到最后.
经过将回调函数放在process.nextTick()
里,代码仍然有执行完的能力,容许全部的变量,函数等先被初始化来供回调函数调用.它还有不容许event loop继续执行的优点.它可能在event loop继续执行前抛出一个错误给用户颇有用.这里提供一个使用process.nextTick()
的示例:
let bar; function someAsyncApiCall(callback) { process.nextTick(callback); } someAsyncApiCall(() => { console.log('bar', bar); // 1 }); bar = 1;
这里有另外一个真实的例子:
const server = net.createServer(() => {}).listen(8080); server.on('listening', () => {});
仅当端口可用时端口当即被绑定.因此'listening'
的回调函数能当即被调用.问题是那时候不会设置.on('listening')
.
为了解决这个问题,'listening'
事件被放入nextTick()
队列来容许代码执行完.这会容许用户设置他们想要的任何事件处理.
process.nextTick()
vs setImmediate()
咱们有两个直到如今用户都关心的类似的调用,但他们的名字使人困惑.
process.nextTick()
在同一阶段当即触发setImmediate()
在如下迭代器或者event loop的'tick'中触发本质上,这两个名字应该交换.process.nextTick()
比setImmediate()
触发要快但这是一个不想改变的历史的命名.作这个改变会破坏npm上大多数包.天天都有新模块被增长,意味着天天咱们都在等待更多的潜在错误发生.当他们困惑时,这个名字就不会被改变.
咱们建议开发者使用setImmediate()
由于它更容易被理解(而且它保持了更好的兼容性,例如浏览器的JS)
process.nextTick()
?有两个主要缘由:
一个知足用户期待的简单例子:
const server = net.createServer(); server.on('connection', function(conn) { }); server.listen(8080); server.on('listening', function() { });
listen()
在event loop开始时执行,可是listening的回调函数被放在一个setImmediate()
中.如今除非主机名可用于绑定端口会当即执行.如今为了event loop继续执行,它必须进入poll阶段,意味着在监听事件前且没有触发容许链接事件时没有接收到请求的可能.
另外一个例子是运行一个函数构造函数,例如,继承自EventEmitter
,而且想要在构造函数中调用一个事件:
const EventEmitter = require('events'); const util = require('util'); function MyEmitter() { EventEmitter.call(this); this.emit('event'); } util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter(); myEmitter.on('event', function() { console.log('an event occurred!'); });
你不能在构造函数中当即触发事件,由于代码不会执行到用户为该事件分配回调函数的地方,因此,在构造函数自己中,你可使用process.nextTick()
设置回调函数来在够咱函数完成后触发事件.有一个小栗子:
const EventEmitter = require('events'); const util = require('util'); function MyEmitter() { EventEmitter.call(this); // use nextTick to emit the event once a handler is assigned process.nextTick(function() { this.emit('event'); }.bind(this)); } util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter(); myEmitter.on('event', function() { console.log('an event occurred!'); });
前面基本是基于文档的翻译(因为英文能力问题,不少地方都模模糊糊,甚至是狗屁不通[捂脸]),下面写一些重点部分的理解
while(true) {}
循环.setTimeout()
,setInterval()
两个定时器,回调执行时间等于或者晚于定时器设定的时间,由于在poll阶段会执行其它回调函数,在空闲时才回去检查定时器(event loop的开始和结束时检查).const fs = require('fs'); fs.readFile('../mine.js', () => { setTimeout(() => { console.log("setTimeout") }, 0); process.nextTick(() => { console.log("process.nextTick") }) setImmediate(() => { console.log("setImmediate") }) }); /*log ------------------- process.nextTick setImmediate setTimeout */
setTimeout
添加至timers队列,解析process.nextTick()
回调函数,将setImmediate
添加至check队列setImmediate
的代码,继续向下一个阶段.process.nextTick()
回调函数setImmediate
setTimeout
回调const fs = require('fs'); const start = new Date(); fs.readFile('../mine.js', () => { setTimeout(() => { console.log("setTimeout spend: ", new Date() - start) }, 0); setImmediate(() => { console.log("setImmediate spend: ", new Date() - start) }) process.nextTick(() => { console.log("process.nextTick spend: ", new Date() - start) }) }); setTimeout(() => { console.log("setTimeout-main spend: ", new Date() - start) }, 0); setImmediate(() => { console.log("setImmediate-main spend: ", new Date() - start) }) process.nextTick(() => { console.log("process.nextTick-main spend: ", new Date() - start) }) /* log ---------------- process.nextTick-main spend: 9 setTimeout-main spend: 12 setImmediate-main spend: 13 process.nextTick spend: 14 setImmediate spend: 15 setTimeout spend: 15 */
这里没有搞懂为何主进程内的setTimeout
老是比setImmediate
先执行,按文档所说,两个应该是不肯定谁先执行.