需要搞懂的 Node.js 的核心 Feature

2,410 阅读5分钟

Node.js的核心Feature

  1. EventLoop 事件循环
  2. global 和 process
  3. Event Emitters 事件触发
  4. Stream 和 Buffer
  5. Cluster 集群
  6. 异步Error
  7. C++ 插件

Event Loop

事件循环算是Node的一个核心了,即使进程中不断有I/O调用也能处理其他任务。正因为阻塞I/O代价太高所以就凸显了Node的高效。

ps: keynote做的图,不会PS,太麻烦。。。

在 Python 这样来实现一个延迟处理

import time

print "Step 1"
print "Step 2"
time.sleep(2)
print "Step 3"

Node或JavaScript 通过异步回调的方式来实现

console.log('Step 1');
setTimeout(function () {
  console.log('Step 3');
}, 2000)
console.log('Step 2');

可以事件循环想象成一个for或while循环,只有在现在或将来没有任务要执行的时候才会停下来。

Blocking IO

在等待I/O任务完成之前就可以做更多事情,事件循环因此让系统更加高效。

Non-Blocking IO

Node也让我们从死锁中解放,因为根本没有锁。

PS:我们仍然可以写出阻塞的代码

var start = Date.now();
for (var i = 1; i<1000000000; i++) {}
var end = Date.now();
console.log(end-start);

这次的阻塞在我的机器上花了3400多毫秒。不过我们多数情况下不会跑一个空循环。

而且fs模块提供了同步(阻塞)和异步(非阻塞)两套处理方法(区别于方法名后是否有Sync)

如下阻塞方式的代码:

var fs = require('fs');

var con1 = fs.readFileSync('1.txt','utf8');
console.log(con1);
console.log('read 1.txt');

var con2 = fs.readFileSync('2.txt','utf8');
console.log(con2);
console.log('read 2.txt');

结果就是

content1->read 1.txt->content2->read 2.txt

非阻塞方式的代码:

var fs = require('fs');

fs.readFile('1.txt','utf8', function(err, contents){
   console.log(contents);
});
console.log('read 1.txt');

fs.readFile('2.txt','utf8', function(err, contents){
   console.log(contents);
});
console.log("read 2.txt");

代码执行后因为要花时间执行 的操作,所以会在最后的回调函数中打印出文件内容。当读取操作结束后事件循环就会拿到内容

read 1.txt->read 2.txt->content1->content2

事件循环的概念对前端工程师比较好理解,关键就是异步、非阻塞I\O。

global

从浏览器端切换到Node端就会出现几个问题

  • 全局变量如何创建(window对象已经没了)
  • 从 CLI 输入的参数、系统信息、内存信息、版本信息等从哪获取

有一个 global 对象,顾名思义就是全局的,它的属性很多

  • global.process 可获取版本信息、CLI 参数、内存使用(process.memoryUsage()这个函数很好用)
  • global.__filename 当前脚本的文件名、路径
  • global.__dirname 当前脚本绝对路径
  • global.module 最常见的模块输出
  • global.require() 模块引入
  • global.console()、setInterval()、setTimeout() 这些浏览器端的方法也在global对象下面

在命令行里执行一次 global 一切就都懂了。

process

通过process对象获取和控制Node自身进程的各种信息。另外process是一个全局对象,在任何地方都可以直接使用。

部分属性

  • process.pid 进程pid
  • process.versions node、v8、ssl、zlib等组件的版本
  • process.arch 系统架构,如:x64
  • process.argv CLI参数
  • process.env 环境变量

部分方法

  • process.uptime() 正常运行的时长
  • process.memoryUsage() 内存使用情况
  • process.cwd() 当前目录
  • process.exit() 退出进程
  • process.on() 添加事件监听 比如:on('uncaughtException')

Events

Events == Node Observer Pattern

异步处理写多了就会出现callbackhell(回调地狱),还有人专门做了叫 callbackhell 的网站

EventEmitter 就是可以触发任何可监听的事件,callbackhell可以通过事件监听和触发来避免。

用法

var events  = require('events');
var emitter = new events.EventEmitter();

添加事件监听和事件触发

emitter.on('eat', function() {
  console.log('eating...');
});

emitter.on('eat', function() {
  console.log('still eating...');
});

emitter.emit('eat');

假设我们有一个已经继承了 EventEmitter 的类,能每周、每天的处理邮件任务,而且这个类具有足够的可扩展性能够自定义最后的输出内容,换言之就是每个使用这个类的人都能够在任务结束时增加自定义的方法和函数。

如下图,我们继承了 EventEmitter 模块创建了一个Class: Job,然后通过事件监听器 done 来实现Job的自定义处理方式。

我们需要做的只是在进程结束的时候触发 done 事件:

// job.js
var util = require('util');
var Job = function() {
  var job = this;
  job.process = function() {
    job.emit('done', { completeTime: new Date() })
  }
}

util.inherits(Job, require('events').EventEmitter);
module.exports = Job;

我们的目的是在 Job 任务结束时执行自定义的函数方法,因此我们可以监听 done 事件然后添加回调:

var Job = require('./job.js')
var job = new Job()

job.on('done', function(data){
  console.log('Job completed at', data.completeTime)
  job.removeAllListeners()
})

job.process()

关于 emitter 还有这些常用方法

  • emitter.listeners(eventName) 列出 eventName 的所有监听器
  • emitter.once(eventName, listener) 只监听一次
  • emitter.removeListener(eventName, listener) 删除监听器

stream 流

用Node处理比较大的数据时可能会出现些问题:

  • 速度较慢
  • 缓冲器只限制在1GB上等,
  • 数据连续不断的时如何何操作

用Stream就会解决。因为Node的 Stream 是对连续数据进行分块后的一个抽象,也就是不需要等待资源完全加载后再操作。

标准Buffer的处理方式:

只有整个Buffer加载完后才能进行下一步操作,看图对比下Node的Stream,只要拿到数据的第一个 chunk 就可以进行处理了

Node中有四种数据流:

  1. Readable 读
  2. Writable 写
  3. Duplex 读&写
  4. Transform 数据转换

Stream在Node中很常见:

  • HTTP 的 request response
  • 标准 I/O
  • 文件读写

Readable Stream

process.stdin 是一个标准输入流,数据一般来自于键盘输入,用它来实现一个可读流的例子。

我们使用 dataend 事件从 stdin 中读取数据。其中 data 事件的回调函数的参数就是 chunk 。

process.stdin.resume()
process.stdin.setEncoding('utf8')

process.stdin.on('data', function (chunk) {
  console.log('chunk: ', chunk)
})

process.stdin.on('end', function () {
  console.log('--- END ---')
})

PS: stdin 默认是处于pause状态的,要想读取数据首先要将其 resume()

可读流还有一个 同步 的 read() 方法,当流读取完后会返回 chunk或null ,课这样来用:

var readable = getReadableStreamMethod()
readable.on('readable', () => {
  var chunk
  while (null !== (chunk = readable.read())) {
    console.log('got %d bytes of data', chunk.length)
  }
})

我们在Node中要尽可能写异步代码避免阻塞线程,不过好在 chunk 都很小所以不用担心同步的 read() 方法把线程阻塞

Writable Stream

我们用 process.stdin 对应的 process.stdout 方法来实现个例子

process.stdout.write('this is stdout data');

把数据写入标准输出后是在命令行中可见的,就像用 console.log()

Pipe

就像自来水要有自来水管一样,Stream 需要传送 也需要 Pipe。

下面的代码就是从文件中读数据,然后GZip压缩,再把数据写入文件

const r = fs.createReadStream('file.txt')
const z = zlib.createGzip()
const w = fs.createWriteStream('file.txt.gz')
r.pipe(z).pipe(w)

readable.pipe() 方法从可读流中拉取所有数据,并写入到目标流中,同时返回目标流,因此可以链式调用(也可以叫导流链)