🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] # 核心 nodejs的核心特性是异步 I/O,事件驱动; # 同步I/O 和 异步I/O 那么同步 I/O 和异步 I/O 又有什么区别么?是不是只要做到非阻塞 IO 就可以实现异步I/O呢? 其实不然。 * 同步 I/O(synchronous I/O)做I/O operation 的时候会将 process 阻塞,所以阻塞 I/O,非阻塞I/O,IO 多路复用I/O都是同步I/O。 * 异步I/O(asynchronous I/O)做I/O opertaion的时候将不会造成任何的阻塞。 非阻塞 I/O 都不阻塞了为什么不是异步 I/O 呢?其实当非阻塞 I/O 准备好数据以后还是要阻塞住进程去内核拿数据的,所以算不上异步I/O。 ![](https://box.kancloud.cn/2d7692f530bd9e6844b251332c4616b8_614x327.png) # 单线程 在 Java、PHP 或者 .net 等服务器端语言中,会为每一个客户端连接创建一个新的线程。而每个线程需要耗费大约2MB内存。也就是说,理论上,一个8GB 内存的服务器可以同时连接的最大用户数为4000个左右。要让Web应用程序支持更多的用户,就需要增加服务器的数量,而 Web 应用程序的硬件成本当然就上升了。 Node.js 不为每个客户连接创建一个新的线程,而仅仅使用一个线程。当有用户连接了,就触发一个内部事件,通过非阻塞`I/O、事件驱动机制`,让 Node.js 程序宏观上也是并行的。使用 Node.js ,一个8GB内存的服务器,可以同时处理超过4万用户的连接。 另外,单线程带来的好处,操作系统完全不再有线程创建、销毁的时间开销。但是单线程也有很多弊端,会在 Node.js 的弊端详细讲解,请继续看。 Node.js 对 http 服务的模型: ![](https://img.kancloud.cn/87/74/8774e3b8672200e1397b0308d1700af7_343x334.png) Node.js的单线程 指的是**主线程是“单线程”**,由主要线程去按照编码顺序一步步执行程序代码,假如遇到同步代码阻塞,主线程被占用,后续的程序代码执行就会被卡住。实践一个测试代码: ``` var http = require('http');function sleep(time) {  var _exit = Date.now() + time * 1000;  while( Date.now() < _exit ) {}     return ; } var server = http.createServer(function(req, res){     sleep(10);     res.end('server sleep 10s'); }); server.listen(8080); ``` 下面为代码块的堆栈图: ![](https://img.kancloud.cn/62/61/6261f4ff4e61aefb4d6baf3ab69c977e_380x473.png) 先将`index.js`的代码改成这样,然后打开浏览器,你会发现浏览器在10秒之后才做出反应,打出`Hello Node.js`。 **JavaScript是解析性语言,代码按照编码顺序一行一行被压进 stack 里面执行,执行完成后移除然后继续压下一行代码块进去执行。** 上面代码块的堆栈图,当主线程接受了 request 后,程序被压进同步执行的 sleep 执行块(我们假设这里就是程序的业务处理),如果在这 10s 内有第二个request进来就会被压进stack里面等待 10s 执行完成后再进一步处理下一个请求,后面的请求都会被挂起等待前面的同步执行完成后再执行。 那么我们会疑问:为什么一个单线程的效率可以这么高,同时处理数万级的并发而不会造成阻塞呢?就是我们下面所说的 -------- **事件循环机制**。 # EventLoop(事件循环机制) 根据 Node.js官方介绍,每次事件循环都包含了**6个阶段(Phase)**,对应到 libuv 源码中的实现,如下图所示: ![](https://box.kancloud.cn/86b4f2c7b6b687f72e31a5a0c78a4dce_1080x486.png) *每个框框里每一步都是事件循环机制的一个阶段。* EventLoop 的每一次循环都需要依次经过上述的阶段。 ~~~shell | nextTick(队列执行) │ ┌──────────┴────────────┐ │ │ timers │ │ └──────────┬────────────┘ | nextTick(队列执行) │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ | nextTick(队列执行) │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ | nextTick(队列执行) ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ | | | nextTick(队列执行) │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ | nextTick(队列执行) │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘ 1. 在进入事件循环的每个阶段前(timers queue, IO events queue, immediates queue, close handlers queue),Node 会检查`nextTick`queue . 如果 queue 不为空, Node 会执行`nextTick`queue 中的任务,直至队列为空,然后才会进入下一阶段。 2. 每个阶段都有自己的 callback 队列,所以进入某个阶段后,会从所属的队列中取出 callback 来执行,**当队列为空或者被执行 callback 的数量达到系统的最大数量时,进入下一阶段**。 ~~~ 这六个阶段都执行完毕称为一轮循环。 > 参考:[Node事件循环系列——2、Timer 、Immediate 和 nextTick](https://zhuanlan.zhihu.com/p/87579819) ## 阶段概览 * **timers(定时器)** : 此阶段执行那些由 `setTimeout()` 和 `setInterval()` 调度的回调函数; 当使用 setTimeout 或者 setInterval 指定延迟的时间到达之后,会将任务添加到 timers 队列中,等待其他阶段的任务队列中的执行完成之后执行。**所以实际脚本执行的时刻 >= 设定的时间,因为此时可能还有其他的任务在队列中等待执行** * **I/O callbacks(I/O回调)** : 此阶段会执行几乎所有的回调函数, 除了 **close callbacks(关闭回调)** 和 那些由 **timers** 与 `setImmediate()` 调度的回调; * **idle(空转), prepare** : 此阶段仅 node 内部使用;与我们编程关系不大。 * **poll(轮询)** : 检索新的 I/O 事件; 在恰当的时候 Node 会阻塞在这个阶段 poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情 * 回到 timer 阶段执行回调 * 执行 I/O 回调 并且在进入该阶段时如果没有设定的 timer 的话,会发生以下两件事情 * 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制 * 如果 poll 队列为空时,会有两件事发生 * 如果有 `setImmediate` 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调 * 如果没有 `setImmediate` 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去 当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会绕回到 timer 阶段执行回调。 * **check(检查)** : 此阶段只处理 `setImmediate()` 设置的回调;因为 Poll 阶段可能设置一些回调, 希望在 Poll 阶段后运行. 所以在 Poll 阶段后面增加了这个 Check 阶段。 * **close callbacks(关闭事件的回调)**: 诸如 `socket.on('close', ...)` 此类的回调在此阶段被调用;用于资源清理。 每个阶段都有一个 **FIFO 队列**来**执行回调**。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后在该阶段的队列中执行回调,直到队列用尽或**最大回调数**已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段,等等。 那么我们平常的异步 io 是在哪个阶段执行的呢,答案是 poll 阶段。 实例: ``` 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毫秒完成,执行回调函数需要10ms,setTimeout 设定为 100ms 后执行。 那么程序的执行顺序如下: 1. [0-95]ms 事件队列为空,nodejs 保持等待 2. [95-105]ms 执行 readFile 的回调 3. 105ms 执行 setTimeout 的回调 4. 在100ms时,setTimeout 到达指定的事件阈值,被放入 timer 阶段的队列中,等待pool 阶段的任务(这里是readFile)结束了之后再被调用。 ## nextTickQueue & microTaskQueue ![node事件循环机制](https://box.kancloud.cn/1675f449c8fd95f81d9f5ea81e936d15_2024x1598.png) 对于日常开发来说,我们比较关注的是 timers、I/O callbacks、check 阶段。 node 和浏览器相比一个明显的不同就是**node 在每个阶段结束后都会去执行所有 microtask 任务**。 对于这个特点,可以做个试验: ``` console.log('main'); setImmediate(function() { console.log('setImmediate'); }); new Promise(function(resolve, reject) { resolve(); }).then(function() { console.log('promise.then'); }); ``` 代码的执行结果是: ``` main promise.then setImemediate ``` 在 node 事件循环的每一个子阶段退出之前都会按顺序执行如下过程: * .... * 检查是否有 `process.nextTick` 回调,如果有,全部执行。 * 检查是否有 `microtaks`,如果有,全部执行。 * 退出当前阶段。 # `process.nextTick` 和 `setImmediate` `process.nextTick` 并不是事件循环中的一部分,指定的回调函数将会被加入到 **nextTickQueue 队列**中。 nextTickQueue 将在完成当前阶段迭代之后和开始下一次事件循环迭代之前处理完所有回调。具体来说,就是在一段代码执行时候,会先检查 nextTickQueue,如果 nextTickQueue 中有回调,会先**执行完 nextTickQueue 中的所有回调**,而不管现在是在事件循环的哪一个阶段。 通过递归使用 `process.nextTick` 可以阻止事件循环。 相对于浏览器环境,node 环境下多出了 `setImmediate` 和 `process.nextTick` 这两种异步操作。 `setImmediate` 的回调函数是被放在 check 阶段执行,即相当于事件循环的最后阶段了。而 `process.nextTick` 会被当做一种 **microtask**,前面提到每个阶段结束后都会执行所有 microtask 任务,所以 `process.nextTick` 有种类似于插队的作用,可以赶在下个阶段前执行,但它和 `promise.then` 哪个先执行呢?通过一段代码来实验: ``` console.log('main'); process.nextTick(function() { console.log(‘nextTick’) }) new Promise(function(resolve, reject) { resolve(); }).then(function() { console.log('promise.then'); }); ``` 代码的执行结果是: ``` main nextTick promise.then ``` 事实证明,**`process.nextTick` 的优先级会比 `promise.then` 高**。 ## `process.nextTick` 的饥饿陷阱 `process.nextTick`的优势在于它能够插入到每个阶段之后,在当前阶段执行完毕后就能立马执行。然而它的这个优点也导致了如果调用不当就容易陷入饥饿陷阱。具体就是当递归地调用 `process.nextTick` 的时候,事件循环一直无法进入到下一个阶段,导致了后面阶段的事件一直无法被执行,产生饥饿问题。 看一个例子就很容易明白 ``` let i = 0; setImmediate(function() { console.log('setImmediate'); }); function callback() { console.log(‘nextTick’ + i++); if (i < 1000) { process.nextTick(callback); } } callback(); ``` 执行的结果是: `nextTick0 nextTick1 nextTick2 … nextTick999 setImmediate` `setImmediate` 的回调会一直等待到 `process.nextTick` 任务都完成后才能被执行。 ## 小结 1. node 的事件循环机制和浏览器的有所不同,多出了 `setImmediate` 和 `process.nextTick` 这两种异步方式。由于`process.nextTick` 会导致 **I/O 饥饿**,所以官方也推荐使用 `setImmediate`。 2. node 虽然是单线程的设计,但它也能实现高并发。原因在于它的主线程事件循环机制和底层线程池的实现。 3. 这种机制决定了 node 比较适合 I/O 密集型应用,而不适合 CPU 密集型应用。 4. `process.nextTick()` 是 node 早期版本无 `setImmediate` 时的产物,node 作者推荐我们尽量使用 `setImmediate`。 > 官网:We recommend developers use`setImmediate()`in all cases… # EventEmitter 和事件循环的关系 EventEmitter 实现了发布订阅模式,但是 EventEmitter 回调函数的执行本身不是异步的,当 `event.emit(‘event’)` 执行的时候,**所有订阅了event事件的回调函数会立即执行(按序)**。但是我们监听 tcp 连接的时候,连接的回调函数时按顺序执行的,前面的连接会阻塞后面的响应,这是因为使用了 `process.nextTick` 或者 `setImmediate`。 比如: ``` http.on('request', function(req,res){ ... }) ``` 如果 request被 `while` 循环阻塞,那么后面的 http 请求都会在 pending 状态,因为此时他们正在队列中。 下面是另一个例子: ``` const EventEmitter = require('events'); const util = require('util'); function MyEmitter() { EventEmitter.call(this); process.nextTick(() => { this.emit('event'); }) } util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('an event occurred!'); }); ``` 上面将 `this.emit(‘event’)` 放入 nextTickQueue 队列,所以注册 event 事件的代码会先执行,这样 event 事件被 `emit` 的时候,事件是被注册了的。 # 线程 Node.js是单线程的,除了系统 I/O 之外,在它的事件轮询过程中,同一时间只会处理一个事件。你可以把事件轮询想象成一个大的队列,在每个**时间点**上,系统只会处理一个事件。即使你的电脑有多个 CPU 核心,你也无法同时并行的处理多个事件。但也就是这种特性使得node.js适合处理 I/O 型的应用,不适合那种 CPU 运算型的应用。在每个 I/O 型的应用中,你只需要给每一个输入输出定义一个回调函数即可,他们会自动加入到事件轮询的处理队列里。当 I/O 操作完成后,这个回调函数会被触发。然后系统会继续处理其他的请求。 node.js单线程只是一个js主线程,本质上的异步操作还是由线程池完成的,node 将所有的阻塞操作都交给了内部的线程池去实现,本身只负责不断的往返调度,并没有进行真正的 I/O 操作,从而实现异步非阻塞 I/O,这便是node单线程和事件驱动的精髓之处了。 **cpu核数与线程之间的关系** 在过去单CPU时代,单任务在一个时间点只能执行单一程序。之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程。虽然并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个CPU,并交由操作系统来完成多任务间对CPU的运行切换,以使得每个任务都有机会获得一定的时间片运行。而现在多核CPU的情况下,同一时间点可以执行多个任务,具体到这个任务在CPU哪个核上运行跟操作系统和 CPU 本身的设计相关 ## 线程驱动和事件驱动 * **线程驱动**就是当收到一个请求的时候,将会为该请求开一个新的线程来处理请求。一般存在一个线程池,线程池中有空闲的线程,会从线程池中拿取线程来进行处理,如果线程池中没有空闲的线程,新来的请求将会进入队列排队,直到线程池中空闲线程。 * **事件驱动**就是当进来一个新的请求的时,请求将会被压入队列中,然后通过一个循环来检测队列中的事件状态变化,如果检测到有状态变化的事件,那么就执行该事件对应的处理代码,一般都是回调函数。 对于事件驱动编程来说,**如果某个时间的回调函数是计算密集型,或者是阻塞I/O,那么这个回调函数将会阻塞后面所有事件回调函数的执行**。这一点尤为重要 。 示例: ``` var fs = require("fs"); var debug = require('debug')('example4'); debug("begin"); setTimeout(function(){ debug("timeout1"); /** * 模拟计算密集 */ for(var i = 0 ; i < 1000000 ; ++i){ for(var j = 0 ; j < 100000 ; ++j); } }); setTimeout(function(){ debug("timeout2"); }); debug('end'); /** Sat, 21 May 2016 08:53:27 GMT example4 begin Sat, 21 May 2016 08:53:27 GMT example4 end Sat, 21 May 2016 08:53:27 GMT example4 timeout1 Sat, 21 May 2016 08:54:09 GMT example4 timeout2 // 注意这里的时间晚了好久 */ ``` # 新版本 v11 的 Timers 和 Microtasks Node v11中的新更改与浏览器行为相匹配,从而提高了 浏览器的 JavaScript 在Node.js 中 的可重用性。 但是,这一重大变化可能会破坏明确依赖旧行为的现有 Node.js 应用程序。 因此,如果要升级到 Node v11或更高版本(最好是下一个 LTS v12),您需要认真的注意一下。 > [又被node的eventloop坑了,这次是node的锅](https://juejin.im/post/5c3e8d90f265da614274218a) 示例: ``` let racer1 = function() { setTimeout(() => { console.log("timeout1") Promise.resolve().then(() => console.log('promise resolve1')); process.nextTick(() => console.log('next tick1')) }, 0); setImmediate(() => console.log("immediate1")); process.nextTick(() => console.log("nextTick1")); } let racer2 = function() { process.nextTick(() => console.log("nextTick2")); setTimeout(() => { console.log("timeout2") Promise.resolve().then(() => console.log('promise resolve2')); process.nextTick(() => console.log('next tick2')) }, 0); setImmediate(() => console.log("immediate2")); } let racer3 = function() { setImmediate(() => console.log("immediate3")); process.nextTick(() => console.log("nextTick3")); setTimeout(() => console.log("timeout3"), 0); } racer1() racer2() racer3() ``` node v10 运行结果: ``` nextTick1 nextTick2 nextTick3 timeout1 timeout2 timeout3 next tick1 next tick2 promise resolve1 promise resolve2 immediate1 immediate2 immediate3 ``` node v11 运行结果: ``` nextTick1 nextTick2 nextTick3 timeout1 next tick1 promise resolve1 timeout2 next tick2 promise resolve2 timeout3 immediate1 immediate2 immediate3 ``` # 工作流示例 ## `setTimeout`和`setImmediate` ``` setTimeout(function timeout () { console.log('timeout'); },0); setImmediate(function immediate () { console.log('immediate'); }); ``` 上述二者的执行顺序是不确定的。 因为在 node 中,`setTimeout(cb, 0) === setTimeout(cb, 1);` 而 `setImmediate` 属于 uv_run_check 的部分,确实每次 loop进来,都是先检查 uv_run_timer 的,但是由于 cpu 工作耗费时间,比如第一次获取的 hrtime 为 0 那么 `setTimeout(cb, 1)` ,超时时间就是 loop->time = 1(ms,node 定时器精确到 1ms,但是 hrtime 是精确到纳秒级别的)所以第一次loop进来的时候就有两种情况: > 1.由于第一次 loop 前的准备耗时超过 1ms,当前的 loop->time >=1 ,则 `uv_run_timer` 生效,timeout 先执行 > 2.由于第一次 loop 前的准备耗时小于 1ms,当前的 loop->time < 1,则本次loop中的第一次 `uv_run_timer` 不生效,那么 `io_poll` 后先执行 `uv_run_check` ,即 immediate 先执行,然后等 `close cb` 执行完后,继续执行 `uv_run_timer` 但当二者在异步 i/o callback 内部调用时,总是先执行 `setImmediate`,再执行 `setTimeout` ``` var fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') }) }) ``` 因为 `fs.readFile` callback 执行完后,程序设定了 timer 和 `setImmediate`,因此 poll 阶段不会被阻塞进而进入 check 阶段先执行 `setImmediate`,最后close callbacks 阶段结束后检查 timer,执行 `timeout` 事件。 ## `process.nextTick` ``` var fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); process.nextTick(()=>{ console.log('nextTick3'); }) }); process.nextTick(()=>{ console.log('nextTick1'); }) process.nextTick(()=>{ console.log('nextTick2'); }) }); // 输出: nextTick1 nextTick2 setImmediate nextTick3 setTimeout ``` 1. 从 poll —> check 阶段,先执行 `process.nextTick`,nextTick1,nextTick2。 2. 然后进入 check 执行 `setImmediate`,`setImmediate` 执行完后,出 check,进入close callback 前,执行 `process.nextTick`,nextTick3。 3. 最后进入 timer 执行 `setTimeout ` # 参考 [浏览器与Node的事件循环(Event Loop)有何区别?](https://cnodejs.org/topic/5c3d554fa4d44449266b1077) [一次弄懂Event Loop(彻底解决此类面试问题)](https://zhuanlan.zhihu.com/p/55511602) [nodejs 异步I/O和事件驱动](https://blog.csdn.net/ii1245712564/article/details/51473803) [Node.js event loop workflow & lifecycle in low level](http://voidcanvas.com/nodejs-event-loop/) [Node.js: How even quick async functions can block the Event-Loop, starve I/O](https://snyk.io/blog/nodejs-how-even-quick-async-functions-can-block-the-event-loop-starve-io/) [Node.js event loop architecture](https://medium.com/preezma/node-js-event-loop-architecture-go-deeper-node-core-c96b4cec7aa4) [Nodejs:单线程为什么能支持高并发?](https://www.cnblogs.com/linzhanfly/p/9082895.html) [Event Loop and the Big Picture — NodeJS Event Loop Part 1](https://blog.insiderattack.net/event-loop-and-the-big-picture-nodejs-event-loop-part-1-1cb67a182810) [深入了解nodejs的事件循环机制](https://blog.csdn.net/li420520/article/details/82900716) [How Node Event Loop REALLY Works: Or Why Most of the Event Loop Diagrams are WRONG](https://webapplog.com/event-loop/)