Node.js开发指南 -01

简介

用异步式IO和事件驱动代替多线程,带来了客观的性能提升

Node.js 使用了单线程、非阻塞的事件编程模式。

异步式 I/O (Asynchronous I/O)或非阻塞式 I/O (Non-blocking I/O)则针对所有 I/O 操作不采用阻塞的策略。当线程遇到 I/O 操作时,不会以阻塞的方式等待 I/O 操作的完成或数据的返回,而只是将 I/O 请求发送给操作系统,继续执行下一条语句。当操作系统完成 I/O 操作时,以事件的形式通知执行 I/O 操作的线程,线程会在特定时候处理这个事件。为了处理异步 I/O,线程必须有事件循环,不断地检查有没有未处理的事件,依次予以处理。

阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。而非阻塞模式下,一个线程永远在执行计算操作,这个线程所使用的 CPU 核心利用率永远是 100%,I/O 以事件的方式通知

在阻塞模式下,多线程往往能提高系统吞吐量,因为一个线程阻塞时还有其他线程在工作,多线程可以让 CPU 资源不被阻塞中的线程浪费。而在非阻塞模式下,线程不会被 I/O 阻塞,永远在利用 CPU。多线程带来的好处仅仅是在多核 CPU 的情况下利用更多的核,而Node.js的单线程也能带来同样的好处。这就是为什么 Node.js 使用了单线程、非阻塞的事件编程模式。

多线程同步式 I/O 与单线程异步式 I/O 的示例
在这里插入图片描述
异步式 I/O 就是少了多线程的开销

对操作系统来说,创建一个线程的代价是十分昂贵的,需要给它分配内存、列入调度,同时在线程切换的时候还要执行内存换页,CPU 的缓存被清空,切换回来的时候还要重新从内存中读取信息,破坏了数据的局部性。

在这里插入图片描述
在这里插入图片描述
fs.readFile 接收了三个参数,
第一个是文件名,第二个是编码方式,第三个是一个函数,我们称这个函数为回调函数。
JavaScript 支持匿名的函数定义方式,譬如我们例子中回调函数的定义就是嵌套在
fs.readFile 的参数表中的。这种定义方式在 JavaScript 程序中极为普遍,与下面这种定义
方式实现的功能是一致的:
在这里插入图片描述
fs.readFile 调用时所做的工作只是将异步式 I/O 请求发送给了操作系统,然后立即
返回并执行后面的语句,执行完以后进入事件循环监听事件。当 fs 接收到 I/O 请求完成的
事件时,事件循环会主动调用回调函数以完成后续工作。因此我们会先看到 end. ,再看到
file.txt 文件的内容。

Node.js 的事件循环对开发者不可见
在这里插入图片描述

模块和包

模块(Module)和包(Package)是 Node.js 最重要的支柱。开发一个具有一定规模的程
序不可能只用一个文件,通常需要把各个功能拆分、封装,然后组合起来,模块正是为了实
现这种方式而诞生的
我们经常把 Node.js 的模块和包相提并论,因为模块和包是没有本质区别的,两个概念
也时常混用。如果要辨析,那么可以把包理解成是实现了某个功能模块的集合,用于发布
和维护。对使用者来说,模块和包的区别是透明的,因此经常不作区分

什么是模块

模块是 Node.js 应用程序的基本组成部分,文件和模块是一一对应的。
换言之,一个Node.js 文件就是一个模块,这个文件可能是 JavaScript 代码、JSON 或者编译过的 C/C++ 扩展。在前面章节的例子中,我们曾经用到了 var http = require(‘http’), 其中 http是 Node.js 的一个核心模块,其内部是用** C++** 实现的,外部用 JavaScript 封装。我们通过require 函数获取了这个模块,然后才能使用其中的对象。

  1. 创建模块
    在 Node.js 中,因为一个文件就是一个模块,我们要关注的问
    题仅仅在于如何在其他文件中获取这个模块。Node.js 提供了 exports 和 require 两个对
    象,其中 exports 是模块公开的接口, require 用于从外部获取一个模块的接口,即所获
    取模块的 exports 对象。

在这里插入图片描述
在这里插入图片描述
module.js 通过 exports 对象把 setName 和 sayHello 作为模块的访
问接口,在 getmodule.js 中通过 require(’./module’) 加载这个块,然后就可以直接访问 module.js 中 exports 对象的成员函数了

2. 单次加载

上面这个例子有点类似于创建一个对象,但实际上和对象又有本质的区别,因为
require 不会重复加载模块,也就是说无论调用多少次 require, 获得的模块都是同一个。
我们在 getmodule.js 的基础上稍作修改:

//loadmodule.js

var hello1 = require('./module');
hello1.setName('BYVoid');

var hello2 = require('./module');
hello2.setName('BYVoid 2');

hello1.sayHello();

运行后发现输出结果是Hello BYVoid 2 ,这是因为变量 hello1 和 hello2 指向的是同一个实例,因此 hello1.setName 的结果被 hello2.setName 覆盖,最终输出结果是由后者决定的。

覆盖 exports

在这里插入图片描述
在这里插入图片描述

模块接口的唯一变化是使用 module.exports = Hello 代替了 exports.Hello=Hello 。在外部引用该模块时,其接口对象就是要输出的 Hello 对象本身,而不是原先的exports 。事实上, exports 本身仅仅是一个普通的空对象,即 {} ,它专门用来声明接口,本质上是通过它为模块闭包① 的内部建立了一个有限的访问接口。因为它没有任何特殊的地方,所以可以用其他东西来代替,譬如我们上面例子中的 Hello 对象。

创建包

Node.js 的包是一个目录,其中包含一个 JSON 格式的包说明文件 package.json。严格符
合 CommonJS 规范的包应该具备以下特征:
 package.json 必须在包的顶层目录下;
 二进制文件应该在 bin 目录下;
 JavaScript 代码应该在 lib 目录下;
 文档应该在 doc 目录下;
 单元测试应该在 test 目录下。

Node.js 对包的要求并没有这么严格,只要顶层目录下有 package.json,并符合一些规范
即可。当然为了提高兼容性,我们还是建议你在制作包的时候,严格遵守 CommonJS 规范。

1. 作为文件夹的模块

模块与文件是一一对应的。文件不仅可以是 JavaScript 代码或二进制代码,还可以是一
个文件夹。最简单的包,就是一个作为文件夹的模块。下面我们来看一个例子,建立一个叫
做 somepackage 的文件夹,在其中创建 index.js,内容如下:
在这里插入图片描述
然后在 somepackage 之外建立 getpackage.js,内容如下:
在这里插入图片描述
运行 node getpackage.js,控制台将输出结果 Hello. 。
我们使用这种方法可以把文件夹封装为一个模块,即所谓的包。包通常是一些模块的集
合,在模块的基础上提供了更高层的抽象,相当于提供了一些固定接口的函数库。通过定制
package.json,我们可以创建更复杂、更完善、更符合规范的包用于发布。

2 package.json

在前面例子中的 somepackage 文件夹下,我们创建一个叫做 package.json 的文件,内容如
下所示:

{
"main" : "./lib/interface.js"
}

然后将 index.js 重命名为 interface.js 并放入 lib 子文件夹下。以同样的方式再次调用这个包,依然可以正常使用。
Node.js 在调用某个包时,会首先检查包中 package.json 文件的 main 字段,将其作为包的接口模块,如果 package.json 或 main 字段不存在,会尝试寻找 index.js 或 index.node
为包的接口。
package.json 是 CommonJS 规定的用来
描述包
的文件,完全符合规范的 package.json 文件应该含有以下字段。
 name :包的名称,必须是唯一的,由小写英文字母、数字和下划线组成,不能包含空格。
 description :包的简要说明。
 version :符合语义化版本识别
① 规范的版本字符串。
 keywords :关键字数组,通常用于搜索。
 maintainers :维护者数组,每个元素要包含 name 、 email (可选)、 web (可选)字段。
 contributors :贡献者数组,格式与 maintainers 相同。包的作者应该是贡献者
数组的第一个元素。
 bugs :提交bug的地址,可以是网址或者电子邮件地址。
 licenses :许可证数组,每个元素要包含 type (许可证的名称)和 url (链接到
许可证文本的地址)字段。
 repositories :仓库托管地址数组,每个元素要包含 type (仓库的类型,如 git )、
url (仓库的地址)和 path (相对于仓库的路径,可选)字段。
 dependencies :包的依赖,一个关联数组,由包名称和版本号组成。
下面是一个完全符合 CommonJS 规范的 package.json 示例:

{
"name": "mypackage",
"description": "Sample package for CommonJS. This package demonstrates the required
elements of a CommonJS package.",
"version": "0.7.0",
"keywords": [
"package",
"example"
],
"maintainers": [
{
"name": "Bill Smith",
"email": "bills@example.com",
}
],
"contributors": [
{
"name": "BYVoid",
"web": "http://www.byvoid.com/"
}
],
"bugs": {
"mail": "dev@example.com",
"web": "http://www.example.com/bugs"
},
"licenses": [
{
"type": "GPLv2",
"url": "http://www.example.org/licenses/gpl.html"
}
],
"repositories": [
{
"type": "git",
"url": "http://github.com/BYVoid/mypackage.git"
}
],
"dependencies": {
"webkit": "1.2",
"ssl": {
"gnutls": ["1.0", "2.0"],
"openssl": "0.9.8"
}
}
}

Node.js 包管理器

Node.js包管理器,即npm是 Node.js 官方提供的包管理工具,它已经成了 Node.js 包的
标准发布平台,用于 Node.js 包的发布、传播、依赖控制。npm 提供了命令行工具,使你可
以方便地下载、安装、升级、删除包,也可以让你作为开发者发布并维护包。

  1. 获取一个包
    使用 npm 安装包的命令格式为:

npm [install/i] [package_name]

  1. 本地模式和全局模式
    npm在默认情况下会从http://npmjs.org搜索或下载包,将包安装到当前目录的node_modules
    子目录下。

提示: pip 总是以全局模式安装,使包可以供所有的程序使用,而 npm 默认会把包安装到当前目录下。这反映了 npm 不同的设计哲学。如果把包安装到全局,可以提高程序的重复利用程度,避免同样的内容的多份副本,但坏处是难以处理不同的版本依赖。如果把包安装到当前目录,或者说本地,则不会有不同程序依赖不同版本的包的冲突问题,同时还减轻了包作者的 API 兼容性压力,但缺陷则是同一个包可能会被安装许多次。

默认情况下我们使用 npminstall 命令就是采用本地模式,即把包安装到当前目录的 node_modules 子目录下。Node.js
的 require 在加载模块时会尝试搜寻 node_modules 子目录,因此使用 npm 本地模式安装的包可以直接被引用。
npm 还有另一种不同的安装模式被成为全局模式,使用方法为:
npm [install/i] -g [package_name]
与本地模式的不同之处就在于多了一个参数 -g 。

为什么要使用全局模式呢?多数时候并不是因为许多程序都有可能用到它,为了减少多重副本而使用全局模式,而是因为本地模式不会注册 PATH 环境变量。举例说明,我们安装supervisor 是为了在命令行中运行它,譬如直接运行 supervisor script.js,这时就需要在 PATH环境变量中注册 supervisor。npm 本地模式仅仅是把包安装到 node_modules 子目录下,其中的 bin 目录没有包含在 PATH 环境变量中,不能直接在命令行中调用。而当我们使用全局模式安装时,npm 会将包安装到系统目录,譬如 /usr/local/lib/node_modules/,同时 package.json 文件中 bin 字段包含的文件会被链接到 /usr/local/bin/。/usr/local/bin/ 是在 PATH 环境变量中默认定义的,因此就可以直接在命令行中运行 supervisor script.js 命令了。

使用全局模式安装的包并不能直接在 JavaScript 文件中用 require 获得,因为 require 不会搜索 /usr/local/lib/node_modules/。

在这里插入图片描述

3. 创建全局链接

npm 提供了一个有趣的命令 npm link, 它的功能是在本地包和全局包之间创建符号链接。我们说过使用全局模式安装的包不能直接通过 require 使用,但通过 npm link 命令可以打破这一限制。举个例子,我们已经通过 npm install -g express 安装了 express ,这时在工程的目录下运行命令:

$ npm link express
./node_modules/express -> /usr/local/lib/node_modules/express

我们可以在 node_modules 子目录中发现一个指向安装到全局的包的符号链接。通过这种方法,我们就可以把全局包当本地包来使用了。
npm link 命令不支持 Windows。
除了将全局的包链接到本地以外,使用 npm link 命令还可以将本地的包链接到全局。使用方法是在包目录(package.json 所在目录)中运行 npm link 命令。如果我们要开发一个包,利用这种方法可以非常方便地在不同的工程间进行测试。

4. 包的发布

调试

1 命令行调试
Node.js 支持命令行下的单步调试。下面是一个简单的程序:

var a = 1;
var b = 'world';
var c = function(x) {
console.log('hello ' + x + a);
};
c(b);

在命令行下执行 node debug debug.js ,将会启动调试工具:
< debugger listening on port 5858
connecting… ok
break in /home/byvoid/debug.js:1
1 var a = 1;
2 var b = ‘world’;
3 var c = function(x) {
debug>
这样就打开了一个 Node.js 的调试终端,我们可以用一些基本的命令进行单步跟踪调试,

在这里插入图片描述
2 远程调试
V8 提供的调试功能是基于 TCP 协议的,因此 Node.js 可以轻松地实现远程调试。在命令行下使用以下两个语句之一可以打开调试服务器:

node --debug [= port ] script.js
node --debug-brk [= port ] script.js

node --debug 命令选项可以启动调试服务器,默认情况下调试端口是 5858也可以
使用 --debug=1234 指定调试端口为 1234。
使用 --debug 选项运行脚本时,脚本会正常执行,但不会暂停,在执行过程中调试客户端可以连接到调试服务器。如果要求脚本暂停执行等待客户端连接,则应该使用 --debug-brk 选项。这时调试服务器在启动后会立刻暂停执行脚本,等待调试客户端连接。当调试服务器启动以后,可以用命令行调试工具作为调试客户端连接,例如:

//在一个终端中
$ node --debug-brk debug.js
debugger listening on port 5858
//在另一个终端中
$ node debug 127.0.0.1:5858
connecting... ok
debug> n
break in /home/byvoid/debug.js:2
1 var a = 1;
2 var b = 'world';
3 var c = function (x) {
4 console.log('hello ' + x + a);
debug>

事实上,当使用 node debug debug.js 命令调试时,只不过是用 Node.js 命令行工
具将以上两步工作自动完成而已。

3 使用 Eclipse 调试 Node.js(p52

4 使用 node-inspector 调试 Node.js大部分基于 Node.js 的应用都是运行在浏览器中的,例如强大的调试工具 node-inspector。node-inspector 是一个完全基于 Node.js 的开源在线调试工具,提供了强大的调试功能和友好的用户界面,它的使用方法十分简便。
首先,使用
npm install -g node-inspector
命令安装 node-inspector,然后在终端中通过 node --debug-brk=5858 debug.js 命令连接你要除错的脚本的调试服务器,启动 node-inspector:
$ node-inspector
在浏览器中打开 http://127.0.0.1:8080/debug?port=5858 , 即可显示出优雅的 Web 调试工

在这里插入图片描述
node-inspector 使用了 WebKit Web Inspector,因此只能在 Chrome、Safari等 WebKit 内核的浏览器中使用,而不支持 Firefox 或 Internet Explorer。

核心模块

process

process 是一个全局变量,即 global 对象的属性。它用于描述当前 Node.js 进程状态的对象,提供了一个与操作系统的简单接口。通常在你写本地命令行程序的时候,少不了要和它打交道。下面将会介绍 process 对象的一些最常用的成员方法。
 process.argv 是命令行参数数组,第一个元素是 node, 第二个元素是脚本文件名,从第三个元素开始每个元素是一个运行参数。
console.log(process.argv);
将以上代码存储为 argv.js,通过以下命令运行:

$ node argv.js 1991 name=byvoid --v "Carbo Kuo"
[ 'node',
'/home/byvoid/argv.js',
'1991',
'name=byvoid',
'--v',
'Carbo Kuo' ]

 process.stdout 是标准输出流,通常我们使用的 console.log() 向标准输出打印字符,而 process.stdout.write() 函数提供了更底层的接口。
 process.stdin 是标准输入流,初始时它是被暂停的,要想从标准输入读取数据,
你必须恢复流,并手动编写流的事件响应函数。

process.stdin.resume();
process.stdin.on('data', function(data) {
process.stdout.write('read from console: ' + data.toString());
});

 process.nextTick(callback) 的功能是为事件循环设置一项任务,Node.js 会在下次事件循环调响应时调用 callback 。
初学者很可能不理解这个函数的作用,有什么任务不能在当下执行完,需要交给下次事件循环响应来做呢?我们讨论过,Node.js 适合== I/O 密集型的应用,而不是计算密集型的应用,因为一个 Node.js 进程只有一个线程,因此在任何时刻都只有一个事件在执行。如果这个事件占用大量的 CPU 时间,执行事件循环中的下一个事件就需要等待很久,因此 Node.js 的一个编程原则就是尽量缩短每个事件的执行时间==。 process.nextTick() 提供了一个这样的
工具,可以把复杂的工作拆散,变成一个个较小的事件。

function doSomething(args, callback) {
somethingComplicated(args);
callback();
}
doSomething(function onEnd() {
compute();
});

假设 compute() 和 somethingComplicated() 是两个较为耗时的函数,以上的程序在调用 doSomething() 时会先执行 somethingComplicated() ,然后立即调用回调函数,在 onEnd() 中又会执行 compute() 。下面用 process.nextTick() 改写上面的程序:

function doSomething(args, callback) {
somethingComplicated(args);
process.nextTick(callback);
}
doSomething(function onEnd() {
compute();
});

改写后的程序会把上面耗时的操作拆分为两个事件,减少每个事件的执行时间,提高事件响应速度。

console

console 用于提供控制台标准输出,它是由 Internet Explorer 的 JScript 引擎提供的调试工具,后来逐渐成为浏览器的事实标准。Node.js 沿用了这个标准,提供与习惯行为一致的console 对象,用于向标准输出流( stdout )或标准错误流( stderr )输出字符。
 console.log() :向标准输出流打印字符并以换行符结束。 console.log 接受若干个参数,如果只有一个参数,则输出这个参数的字符串形式。如果有多个参数,则以类似于 C 语言 printf() 命令的格式输出。第一个参数是一个字符串,如果没有参数,只打印一个换行。

console.log('Hello world');
console.log('byvoid%diovyb');
console.log('byvoid%diovyb', 1991);

运行结果为:

Hello world
byvoid%diovyb
byvoid1991iovyb
 console.error() :与 console.log() 用法相同,只是向标准错误流输出。
 console.trace() :向标准错误流输出当前的调用栈。
console . trace ();
运行结果为:

Trace:
at Object.<anonymous> (/home/byvoid/consoletrace.js:1:71)
at Module._compile (module.js:441:26)
at Object..js (module.js:459:10)
at Module.load (module.js:348:31)
at Function._load (module.js:308:12)
at Array.0 (module.js:479:10)
at EventEmitter._tickCallback (node.js:192:40)

常用工具 util

util 是一个 Node.js 核心模块,提供常用函数的集合,用于弥补核心 JavaScript 的功能过于精简的不足。

util.inherits

util.inherits(constructor, superConstructor) 是一个实现对象间原型继承的函数。JavaScript 的面向对象特性是基于原型的,与常见的基于类的不同。JavaScript 没有提供对象继承的语言级别特性,而是通过原型复制来实现的,具体细节我们在附录A中讨论,
在这里我们只介绍 util.inherits 的用法,示例如下:

var util = require('util');
function Base() {
this.name = 'base';
this.base = 1991;
this.sayHello = function() {
console.log('Hello ' + this.name);
};
}
Base.prototype.showName = function() {
console.log(this.name);
};
function Sub() {
this.name = 'sub';
}
util.inherits(Sub, Base);
var objBase = new Base();
objBase.showName();
objBase.sayHello();
console.log(objBase);
var objSub = new Sub();
objSub.showName();
//objSub.sayHello();
console.log(objSub);

我们定义了一个基础对象 Base 和一个继承自 Base 的 Sub , Base 有三个在构造函数内定义的属性和一个原型中定义的函数,通过 util.inherits 实现继承。运行结果如下:

base
Hello base
{ name: 'base', base: 1991, sayHello: [Function] }
sub
{ name: 'sub' }

注意, Sub 仅仅继承了 Base 在原型中定义的函数,而构造函数内部创造的 base 属性和 sayHello 函数都没有被 Sub 继承。同时,在原型中定义的属性不会被 console.log 作为对象的属性输出。如果我们去掉 objSub.sayHello(); 这行的注释,将会看到:

node.js:201
throw e; // process.nextTick error, or 'error' event on first tick
^
TypeError: Object #<Sub> has no method 'sayHello'
at Object.<anonymous> (/home/byvoid/utilinherits.js:29:8)
at Module._compile (module.js:441:26)
at Object..js (module.js:459:10)
at Module.load (module.js:348:31)
at Function._load (module.js:308:12)
at Array.0 (module.js:479:10)
at EventEmitter._tickCallback (node.js:192:40)

util.inspect

util.inspect(object,[showHidden],[depth],[colors])是一个将任意对象转换为字符串的方法,通常用于调试和错误输出。它至少接受一个参数 object ,即要转换的对象。

showHidden 是一个可选参数,如果值为 true ,将会输出更多隐藏信息。
depth 表示最大递归的层数,如果对象很复杂,你可以指定层数以控制输出信息的多
少。如果不指定 depth ,默认会递归2层,指定为 null 表示将不限递归层数完整遍历对象。
如果 color 值为 true ,输出格式将会以 ANSI 颜色编码,通常用于在终端显示更漂亮
的效果。
特别要指出的是,util.inspect 并不会简单地直接把对象转换为字符串,即使该对象定义了 toString 方法也不会调用。

var util = require('util');
function Person() {
this.name = 'byvoid';
this.toString = function() {
return this.name;
};
}
var obj = new Person();
console.log(util.inspect(obj));
console.log(util.inspect(obj, true));
运行结果是:
{ name: 'byvoid', toString: [Function] }
{ toString:
{ [Function]
[prototype]: { [constructor]: [Circular] },
[caller]: null,
[length]: 0,
[name]: '',
[arguments]: null },
name: 'byvoid' }

除了以上我们介绍的几个函数之外, util 还提供了 util.isArray() 、 util.isRegExp() 、util.isDate() 、 util.isError() 四个类型测试工具,以及 util.format() 、 util.debug() 等工具。

事件驱动 events

events 是 Node.js 最重要的模块,没有“之一”,原因是 Node.js 本身架构就是事件式的,而它提供了唯一的接口,所以堪称 Node.js 事件编程的基石。 events 模块不仅用于用户代码与 Node.js 下层事件循环的交互,还几乎被所有的模块依赖

事件发射器

events 模块只提供了一个对象: events.EventEmitter 。 EventEmitter 的核心就是事件发射与事件监听器功能的封装。 EventEmitter 的每个事件由一个事件名和若干个参数组成,事件名是一个字符串,通常表达一定的语义。对于每个事件, EventEmitter 支持若干个事件监听器。当事件发射时,注册到这个事件的事件监听器被依次调用,事件参数作为回调函数参数传递。

让我们以下面的例子解释这个过程:

var events = require('events');
var emitter = new events.EventEmitter();
emitter.on('someEvent', function(arg1, arg2) {
console.log('listener1', arg1, arg2);
});
emitter.on('someEvent', function(arg1, arg2) {
console.log('listener2', arg1, arg2);
});
emitter.emit('someEvent', 'byvoid', 1991);

运行的结果是:
listener1 byvoid 1991
listener2 byvoid 1991

emitter 为事件 someEvent 注册了两个事件监听器,然后发射了
someEvent 事件。运行结果中可以看到两个事件监听器回调函数被先后调用。
这就是 EventEmitter 最简单的用法。接下来我们介绍一下 EventEmitter 常用的API。
 EventEmitter.on(event, listener) 为指定事件注册一个监听器,接受一个字符串 event 和一个回调函数 listener 。
 EventEmitter.emit(event, [arg1], [arg2], […]) 发射 event 事件,传递若干可选参数到事件监听器的参数表。
 EventEmitter.once(event, listener) 为指定事件注册一个单次监听器,即监听器最多只会触发一次,触发后立刻解除该监听器。
 EventEmitter.removeListener(event, listener) 移除指定事件的某个监听器, listener 必须是该事件已经注册过的监听器。
 EventEmitter.removeAllListeners([event]) 移除所有事件的所有监听器,如果指定 event ,则移除指定事件的所有监听器

error 事件

EventEmitter 定义了一个特殊的事件 error ,它包含了“错误”的语义,我们在遇到异常的时候通常会发射 error 事件。当 error 被发射时, EventEmitter 规定如果没有响应的监听器,Node.js 会把它当作异常,退出程序并打印调用栈。我们一般要为会发射 error
事件的对象设置监听器,避免遇到错误后整个程序崩溃。例如:

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

运行时会显示以下错误:

node.js:201
throw e; // process.nextTick error, or 'error' event on first tick
^
Error: Uncaught, unspecified 'error' event.
at EventEmitter.emit (events.js:50:15)
at Object.<anonymous> (/home/byvoid/error.js:5:9)
at Module._compile (module.js:441:26)
at Object..js (module.js:459:10)
at Module.load (module.js:348:31)
at Function._load (module.js:308:12)
at Array.0 (module.js:479:10)
at EventEmitter._tickCallback (node.js:192:40)

继承 EventEmitter

大多数时候我们不会直接使用 EventEmitter ,而是在对象中继承它。包括 fs 、 net 、http 在内的,只要是支持事件响应的核心模块都是 EventEmitter 的子类。
为什么要这样做呢?
原因有两点。首先,具有某个实体功能的对象实现事件符合语义,事件的监听和发射应该是一个对象的方法。其次 JavaScript 的对象机制是基于原型的,支持部分多重继承,继承 EventEmitter 不会打乱对象原有的继承关系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值