Node.js 是基于V8引擎的javascript运行环境. Node.js具备
事件驱动
,非阻塞I/O
等特色. 结合Node API, Node.js 具备网络编程, 文件系统等服务端的功能, Node.js用libuv
库进行异步事件处理.javascript
Node.js的单线程含义, 实际上说的是执行同步代码的主线程. 一个Node程序的启动, 不止是分配了一个线程,而是咱们只能在一个线程执行代码. 当出现I/O资源调用, TCP链接等外部资源申请的时候, 不会阻塞主线程, 而是委托给I/O线程进行处理,而且进入等待队列. 一旦主线程执行完成,将会消费事件队列(Event Queue). 由于只有一个主线程, 只占用CPU内核处理逻辑计算, 所以不适合在CPU密集型进行使用.html
注意,上图的EVENT_QUEUE
给人看起来是只有一个队列, 根据Node.js官方介绍, EventLoop
有6个阶段, 同时每一个阶段都有对应的一个先进先出的回调队列. java
In computer science, the event loop, message dispatcher, message loop, message pump, or run loop is a programming construct that waits for and dispatches events or messages in a program. -- from wikinode
大概含义: EventLoop 是一种经常使用的机制,经过对内部或外部的事件提供者发出请求, 如文件读写, 网络链接 等异步操做, 完成后调用事件处理程序. 整个过程都是异步阶段git
When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop. -- from node.js docgithub
大体含义: 当Node.js 启动, 就会初始化一个 event loop, 处理脚本时, 可能会发生异步API行为调用, 使用定时器任务或者nexTick, 处理完成后进入事件循环处理过程编程
┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
每个阶段都有一个FIFO的callbacks队列, 每一个阶段都有本身的事件处理方式. 当事件循环进入某个阶段时, 将会在该阶段内执行回调,直到队列耗尽或者回调的最大数量已执行, 那么将进入下一个处理阶段. 网络
下面是摘抄creeperyang 对上面6个阶段的 (原文翻译)异步
一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间事后,timers会尽量早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。socket
注意:技术上来讲,poll 阶段控制 timers 何时执行。
注意:这个下限时间有个范围:[1, 2147483647],若是设定的时间不在这个范围,将被设置为1。
这个阶段执行一些系统操做的回调。好比TCP错误,如一个TCP socket在想要链接时收到ECONNREFUSED,
类unix系统会等待以报告错误,这就会放到 I/O callbacks 阶段的队列执行.
名字会让人误解为执行I/O回调处理程序, 实际上I/O回调会由poll阶段处理.
poll 阶段有两个主要功能:
执行下限时间已经达到的timers的回调,而后
处理 poll 队列里的事件。
当event loop进入 poll 阶段,而且 没有设定的timers(there are no timers scheduled),会发生下面两件事之一:
若是 poll 队列不空,event loop会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;
若是 poll 队列为空,则发生如下两件事之一:
可是,当event loop进入 poll 阶段,而且 有设定的timers,一旦 poll 队列为空(poll 阶段空闲状态):
这个阶段容许在 poll 阶段结束后当即执行回调。若是 poll 阶段空闲,而且有被setImmediate()设定的回调,event loop会转到 check 阶段而不是继续等待。
setImmediate()其实是一个特殊的timer,跑在event loop中一个独立的阶段。它使用libuv的API
来设定在 poll 阶段结束后当即执行回调。
一般上来说,随着代码执行,event loop终将进入 poll 阶段,在这个阶段等待 incoming connection, request 等等。可是,只要有被setImmediate()设定了回调,一旦 poll 阶段空闲,那么程序将结束 poll 阶段并进入 check 阶段,而不是继续等待 poll 事件们 (poll events)。
若是一个 socket 或 handle 被忽然关掉(好比 socket.destroy()),close事件将在这个阶段被触发,不然将经过process.nextTick()触发
简单的 EventLoop
const fs = require('fs'); let counts = 0; function wait (mstime) { let date = Date.now(); while (Date.now() - date < mstime) { // do nothing } } function asyncOperation (callback) { fs.readFile(__dirname + '/' + __filename, callback); } const lastTime = Date.now(); setTimeout(() => { console.log('timers', Date.now() - lastTime + 'ms'); }, 0); process.nextTick(() => { // 进入event loop // timers阶段以前执行 wait(20); asyncOperation(() => { console.log('poll'); }); }); /** * result: * timers 21ms * poll */
为了让setTimeout
优先于fs.readFile
回调, 执行了process.nextTick, 表示在进入 timers阶段前, 等待20ms后执行文件读取.
process.nextTick 不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过渡, 即本阶段执行结束, 进入下一个阶段前, 所要执行的回调。有给人一种插队的感受.
setImmediate的回调处于check阶段, 当poll阶段的队列为空, 且check阶段的事件队列存在的时候,切换到check阶段执行.
nextTick 递归的危害
因为nextTick具备插队的机制,nextTick的递归会让事件循环机制没法进入下一个阶段. 致使I/O处理完成或者定时任务超时后仍然没法执行, 致使了其它事件处理程序处于饥饿状态. 为了防止递归产生的问题, Node.js 提供了一个 process.maxTickDepth (默认 1000)。
递归nextTick
const fs = require('fs'); let counts = 0; function wait (mstime) { let date = Date.now(); while (Date.now() - date < mstime) { // do nothing } } function nextTick () { process.nextTick(() => { wait(20); nextTick(); }); } const lastTime = Date.now(); setTimeout(() => { console.log('timers', Date.now() - lastTime + 'ms'); }, 0); nextTick();
此时永远没法跳到timer阶段, 由于在进入timers阶段前有不断的nextTick插入执行. 除非执行了1000次到了执行上限.
setImmediate
若是在一个I/O周期内进行调度,setImmediate()将始终在任何定时器以前执行.
无 I/O 处理状况下
setTimeout(function timeout () { console.log('timeout'); },0); setImmediate(function immediate () { console.log('immediate'); });
输出结果是 不肯定 的!
setTimeout(fn, 0) 具备几毫秒的不肯定性. 没法保证进入timers阶段, 定时器可以当即执行处理程序.
在I/O事件处理程序下
var fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') }) })
此时 setImmediate
优先于 setTimeout
执行,由于 poll阶段执行完成后 进入 check阶段. timers阶段处于下一个事件循环阶段了.