Node.js 的官方文档中有一段对 Node.js 的简介,如下。
Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.
大意就是说 Node.js 是基于 V8 的 JavaScript 运行时,事件驱动、非阻塞,因此轻量、高效。
寥寥数语,并没有说清楚 Node.js 到底是什么。参考了一些 Node.js 的官方文章以及社区里的分析,整理如下。
要想深入理解 Node.js,我们需要把 Node.js 进行必要的拆解,了解每个组成部分的作用,它们之间如何交互,最终构成 Node.js 这个强大的运行时环境。
上图是 Node.js 的内部结构图。我们可以看到,自底向上主要可以分成三层:最底层是 Node.js 依赖的各种库,有 V8、libuv 等;中间层是各种 Binding,也就是胶水代码;最上层是应用代码,可使用 Node.js 的各种 API。
Node.js 底层的依赖库,有的以 C 语言开发,有的以 C++ 语言开发,如何让应用代码(JavaScript)能够与这些底层库相互调用呢?这就需要中间层的 Binding 来完成。Binding 是一些胶水代码,能够把不同语言绑定在一起使其能够互相沟通。在 Node.js 中,binding 所做的就是把 Node.js 那些用 C/C++ 写的库接口暴露给 JS 环境。
中间层中,除了 Binding,还有 Addon。Binding 仅桥接 Node.js 核心库的一些依赖,如果你想在应用程序中包含其他第三方或者你自己的 C/C++ 库的话,需要自己完成这部分胶水代码。你写的这部分胶水代码就称为 Addon。本质上都是完成桥接的作用,使得应用与底层库能够互通有无。
应用层的代码,就不必多言了,我们开发的应用、npm 安装的包都运行在这里。
刚接触 Node.js 的时候,就知道 Node.js 有一个事件循环,类似于
,但是不知道每次循环什么时候开始,什么时候结束,在每次循环中,Node.js 是如何处理同步与异步代码的。
- while(true)
要说事件循环,就不得不先说明一下 Node.js 的工作流程。下图可以简要说明。
一个 Node.js 应用启动时,V8 引擎会执行你写的应用代码,保持一份观察者(注册在事件上的回调函数)列表。当事件发生时,它的回调函数会被加进一个事件队列。只要这个队列还有等待执行的回调函数,事件循环就会持续把回调函数从队列中拿出并执行。
在回调函数执行过程中,所有的 I/O 请求都会转发给工作线程处理。libuv 维持着一个线程池,包含四个工作线程(默认值,可配置)。文件系统 I/O 请求和 DNS 相关请求都会放进这个线程池处理;其他的请求,如网络、平台特性相关的请求会分发给相应的系统处理单元进行处理。
安排给线程池的这些 I/O 操作由 Node.js 的底层库执行,完成之后触发相应事件,对应的事件回调函数会被放入事件队列,等待执行后续操作。这就是一个事件在 Node.js 中执行的整个生命周期。
前面说了,我们只知道 Node.js 有事件循环,但是不知道每次循环何时开始、何时结束。下面就简要说明一下每次循环的处理过程,详细内容请参考 Node.js 官方说明。
一次事件循环,大概可以分为如下几个阶段:
图中每一个方块,在事件循环中被称为一个阶段 (phase)。
每个阶段都有自己独有的一个用于执行回调函数的 FIFO 队列。当事件循环进入一个指定阶段时,会执行队列中的回调函数,当队列中已经被清空或者执行的回调函数个数达到系统最大限制时,事件循环会进入下一个阶段。
上图中总共有 6 个阶段:
和
- setTimeout()
设置的回调函数。
- setInterval()
设置的回调以外的几乎所有的回调。
- setImmediate()
设置的回调。
- setImmediate()
.
- socket.on('close', ...)
这里有个令人困惑的地方,
与
- I/O callbacks
这两个阶段有什么区别? 既然
- poll
中已经把回调都执行完了,还要
- I/O callbacks
做什么?
- poll
查阅了 libuv 的文档后发现,在 libuv 的 event loop 中,
阶段会执行
- I/O callbacks
。绝大多数情况下,在
- Pending callbacks
阶段,所有的 I/O 回调都已经被执行。但是,在某些情况下,有一些回调会被延迟到下一次循环执行。也就是说,在
- poll
阶段执行的回调函数,是上一次事件循环中被延迟执行的回调函数。
- I/O callbacks
还需要提到的一点是
。
- process.nextTick()
产生的回调函数保存在一个叫做
- process.nextTick()
的队列中,不在上面任何一个阶段的队列里面。当当前操作完成后,
- nextTickQueue
中的回调函数会立即被执行,不管事件循环处在哪个阶段。也就是说,在
- nextTickQueue
中的回调函数被执行完毕之前,事件循环不会往前推进。
- nextTickQueue
如下代码中使用了
,
- setTimeout()
,
- setInterval()
,
- setImmediate()
,
- promise
,可借助于输出结果,理解事件循环。
- process.nextTick()
- 'use strict';
- const fs = require('fs');
- console.log('script start');
- const interval = setInterval(() = >{
- console.log('setInterval')
- },
- 500);
- setTimeout(() = >{
- console.log('setTimeout 1');
- Promise.resolve().then(() = >{
- console.log('promise 3');
- }).then(() = >{
- console.log('promise 4');
- process.nextTick(() = >{
- console.log('nextTick 1');
- });
- }).then(() = >{
- setTimeout(() = >{
- console.log('setTimeout 2');
- Promise.resolve().then(() = >{
- console.log('promise 5');
- }).then(() = >{
- console.log('promise 6');
- process.nextTick(() = >{
- console.log('nextTick 2');
- });
- }).then(() = >{
- clearInterval(interval);
- });
- },
- 0);
- });
- },
- 1000);
- Promise.resolve().then(() = >{
- console.log('promise 1');
- }).then(() = >{
- console.log('promise 2');
- });
- setImmediate(() = >{
- console.log('setImmediate 1');
- });
- console.log('script done');
执行结果为:
- script start
- script done
- promise 1
- promise 2
- setImmediate 1
- setInterval
- setTimeout 1
- promise 3
- promise 4
- nextTick 1
- setInterval
- setTimeout 2
- promise 5
- promise 6
- nextTick 2
来源: http://www.cnblogs.com/bingooo/p/6720540.html