Node.js中使用事件发射器模式实现事件绑定详解

在Node里,很多对象都会发射事件。比如,一个TCP服务器,每当有客户端请求连接就会发射“connect”事件,又比如,每当读取一整块数据,文件系统就会发射一个“data”事件。这些对象在Node里被称为事件发射器(event emitter)。事件发射器允许程序员订阅他们感兴趣的事件,并将回调函数绑定到相关的事件上,这样每当事件发射器发射事件时回调函数就会被调用。发布/订阅模式非常类似传统的GUI模式,比如按钮被点击时程序就会收到相应的通知。使用这种模式,服务端程序可以在一些事件发生时作出反应,比如有客户端连接,socket上有可用数据,或者文件被关闭的时候。

还可以创建自己的事件发射器,事实上,Node专门提供了一个EventEmitter伪类,可以把它当作基类来创建自己的事件发射器。

理解回调模式

异步编程不使用函数返回值来表明函数调用的结束,而是采用后继传递风格。

“后继传递风格”(CPS:Continuation-passing style)是一种编程风格,流程控制被显式传递给下一步操作……

CPS风格的函数会接受一个函数作为额外参数,这个函数用来显式指出程序控制的下个流程,当CPS函数计算出它的“返回值”,它就会调用那个代表了程序下个流程的函数,并将CPS函数的“返回值”作为其参数。

出自维基百科——http://en.wikipedia.org/wiki/Continuation-passing_style

这种编程风格里,每个函数在执行结束后都会调用一个回调函数,这样程序就可以继续运行。后面你会明白,JavaScript非常适合这种编程风格,下面是个Node下将文件加载到内存的例子:

代码如下:

var fs = require('fs');

fs.readFile('/etc/passwd', function(err, fileContent) {

if (err) {

throw err;

}

console.log('file content', fileContent.toString());

});

这个例子里,你传递了一个内联匿名函数作为fs.readFile的第二个参数,其实这就是在使用CPS编程,因为你把程序执行的后续流程交给了那个回调函数。

如你所见,回调函数的第一个参数是个错误对象,如果程序发生错误,这个参数将会是一个Error类的实例,这是Node里CPS编程的一个常见模式。

理解事件发射器模式

标准回调模式里,把一个函数作为参数传递给将被执行的函数,这种模式在客户端需要在函数完成后被通知的场景下工作的很好。但是如果函数的执行过程中发生了多个事件或事件重复发生了多次,这种模式就不太适合了。比如,你想在socket每次收到可用数据时得到通知,这种场景你会发现标准回调模式不太好用,这时事件发射器模式就派上用场了,你可以用一套标准接口来清晰的分离事件发生器和事件监听器。

使用事件发生器模式时,会涉及到两个或多个对象——事件发射器和一个或多个事件监听器。

事件发射器,顾名思义,是个可以产生事件的对象。而事件监听器则是绑定到事件发射器上的代码,用来监听特定类型的事件,就像下面的例子:

代码如下:

var req = http.request(options, function(response) {

response.on("data", function(data) {

console.log("some data from the response", data);

});

response.on("end", function() {

console.log("response ended");

});

});

req.end();

这段代码演示了用Node的 http.request API(见后面章节)创建一个HTTP请求来访问远程HTTP服务器时的两个必要步骤。第一行采用了“后继传递风格”(CPS:Continuation-passing style),传递了一个当HTTP响应时会被调用的内联函数。HTTP请求API在这儿使用CPS是因为程序需要在http.request函数执行完毕后才继续执行后续操作。

当http.request执行完毕,就会调用那个匿名回调函数,然后将HTTP响应对象作为参数传递给它,这个HTTP响应对象是个事件发射器,根据Node文档,它可以发射包括data,end在内的很多事件,你注册的那些回调函数会在每次事件发生时被调用。

作为一条经验,当你需要在请求的操作完成后重新获取执行权时使用CPS模式,以及当事件可以发生多次时使用事件发射器模式。

理解事件类型

被发射的事件都有一个用字符串表示的类型,前面的例子包含“data”和“end”两个事件类型,它们是由事件发射器来定义的任意字符串,不过约定俗成的是,事件类型通常都由不包含空字符的小写单词组成。

不能用代码来推断出事件发射器能产生哪些类型的事件,因为事件发射器API并没有内省机制,因此你使用的API应该有文档来表明它能发射那些类型的事件。

一旦事件发生,事件发射器就会调用跟事件相关的监听器,并将相关数据作为参数传递给监听器。在前面http.request那个例子里,“data”事件回调函数接受一个data对象作为它第一个也是唯一的参数,而“end”不接受任何数据,这些参数作为API契约的一部分也是由API的作者主观定义的,这些回调函数的参数签名也会在每个事件发射器的API文档里有说明。

事件发射器虽然是个为所有类型事件服务的接口,不过“error”事件是Node里的一个特殊实现。Node里的大多数事件发射器都会在程序发生错误时产生“error”事件,如果程序没有监听某个事件发射器的 “error”事件,事件发射器将会注意到并在错误发生时向上抛出一个未捕获异常。

你可以在Node PERL里运行下面的代码来测试下效果,它模拟了一个能产生两种事件的事件发射器:

代码如下:

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

em.emit('event1');

em.emit('error', new Error('My mistake'));

你将会看到下面的输出:

代码如下:

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

undefined

> em.emit('event1');

false

> em.emit('error', new Error('My mistake'));

Error: My mistake

at repl:1:18

at REPLServer.eval (repl.js:80:21)

at repl.js:190:20

at REPLServer.eval (repl.js:87:5)

at Interface.<anonymous> (repl.js:182:12)

at Interface.emit (events.js:67:17)

at Interface._onLine (readline.js:162:10)

at Interface._line (readline.js:426:8)

at Interface._ttyWrite (readline.js:603:14)

at ReadStream.<anonymous> (readline.js:82:12)

>

代码第2行,随便发射了一个叫“event1”的事件,没有任何效果,但是当发射“error”事件时,错误被抛出到堆栈。如果程序不是运行在PERL命令行环境里,程序将会因为未捕获的异常而崩溃。

使用事件发射器API

任何实现了事件发射器模式的对象(比如TCP Socket,HTTP 请求等)都实现了下面的一组方法:

代码如下:

.addListener和.on —— 为指定类型的事件添加事件监听器
.once —— 为指定类型的事件绑定一个仅执行一次的事件监听器
.removeEventListener —— 删除绑定到指定事件上的某个监听器
.removeAllEventListener —— 删除绑定到指定事件上的所有监听器

下面我们具体介绍它们。

使用.addListener()或.on()绑定回调函数

通过指定事件类型和回调函数,你可以注册当事件发生时被执行的操作。比如,文件读取数据流时如果有可用的数据块,就会发射一个“data”事件,下面代码展示如何通过传入一个回调函数来让程序告诉你发生了data事件。

代码如下:

function receiveData(data) {

console.log("got data from file read stream: %j", data);

}

readStream.addListener(“data”, receiveData);

你也可以使用.on,它只是.addListener的简写方式,下面的代码和上面的是一样的:

代码如下:

function receiveData(data) {

console.log("got data from file read stream: %j", data);

}
readStream.on(“data”, receiveData);

前面代码,使用事先定义的一个的命名函数作为回调函数,你也可以使用一个内联匿名函数来简化代码:

代码如下:

readStream.on("data", function(data) {

console.log("got data from file read stream: %j", data);

});

前面说过,传递给回调函数的参数个数和签名依赖于具体的事件发射器对象和事件类型,它们并不是被标准化的,“data”事件可能传递的是一个数据缓冲对象,“error”事件传递一个错误对象,数据流的“end”事件不向事件监听器传递任何数据。

绑定多个事件监听器

事件发射器模式允许多个事件监听器监听同一个事件发射器的同一事件类型,比如:

代码如下:

I have some data here.

I have some data here too.

事件发射器负责按监听器的注册顺序调用指定事件类型上绑定的所有监听器,也就是说:

1.当事件发生后事件监听器可能不会被立刻调用,也许会有其它事件监听器在它之前被调用。
2.异常被抛出到堆栈是不正常的行为,可能是因为代码里有bug,当事件被发射时,如果有一个事件监听器在被调用时抛出了异常,可能会导致一些事件监听器永远不会被调用。这种情况下,事件发射器会捕获到异常,也许还会处理它。

看下面这个例子:

代码如下:

readStream.on("data", function(data) {

throw new Error("Something wrong has happened");

});

readStream.on("data", function(data) {

console.log('I have some data here too.');

});

因为第一个监听器抛出了异常,因此第二个监听器不会被调用。

用.removeListener()从事件发射器移除一个事件监听器

如果当你不再关心一个对象的某个事件时,你可以通过指定事件类型和回调函数来取消已注册的事件监听器,像这样:

代码如下:

function receiveData(data) {

console.log("got data from file read stream: %j", data);

}

readStream.on("data", receiveData);

// ...

readStream.removeListener("data", receiveData);

这个例子里,最后一行把一个可能在将来被随时调用的事件监听器从事件发射器对象移除了。

为了删除监听器,你必须给回调函数命名,因为在添加和删除的时候需要回调函数的名字。

使用.once()让回调函数最多执行一次

如果你想监听一个最多执行一次的事件,或者只对某个事件发生的第一次感兴趣,可以用.once()函数:

代码如下:

function receiveData(data) {

console.log("got data from file read stream: %j", data);

}

readStream.once("data", receiveData);

上面的代码,receiveData函数只会被调用一次。如果readStream对象发射了data事件,receiveData回调函数将会而且仅会被触发一次。

它其实只是个方便方法,因为很简单的就能实现它,像这样:

代码如下:

var EventEmitter = require("events").EventEmitter;

EventEmitter.prototype.once = function(type, callback) {

var that = this;

this.on(type, function listener() {

that.removeListener(type, listener);

callback.apply(that, arguments);

});

};

上面代码里,你重新定了EventEmitter.prototype.once函数,同时也重定义了每个继承自EventEmitter的所有对象的once函数。代码只是简单的使用.on()方法,一旦收到了事件,就用.removeEventListener()取消回调函数的注册,并调用原来的回调函数。

注意:前面代码里使用了function.apply()方法,它接受一个对象并把它作为内含的this变量,以及一个参数数组。前面例子里,通过事件发射器把未修改过的参数数组透明地传递给回调函数。

用.removeAllListeners()从事件发射器移除所有事件监听器

你可以像下面那样从事件发射器移除所有注册到指定事件类型上的所有监听器:

代码如下:

emitter.removeAllListeners(type);

比如,你可以这样取消所有进程中断信号的监听器:

代码如下:

process.removeAllListeners("SIGTERM");

注意:作为一条经验,推荐你只在确切知道删除了什么内容时才使用这个函数,否则,应该让应用程序其它部分来删除事件监听器集合,或者也可以让程序的那些部分自己负责移除监听器。但不管怎样,在某些罕见的场景下,这个函数还是很有用的,比如当你准备有序的关闭一个事件发射器或者关闭整个进程的时候。

创建事件发射器

事件发射器用一个很棒的方式让编程接口变得更通用,在一个常见易懂的编程模式里,客户端直接调用各种函数,而在事件发射器模式中,客户端被绑定到各种事件上,这会让你的程序变得更灵活。(译者注:这句不太自信,贴出原文:The event emitter provides a great way of making a programming interface more generic. When you use a common understood pattern, clients bind to events instead of invoking functions, making your program more flexible.)

此外,通过使用事件发射器,你还可以获得许多特性,比如在同一事件上绑定多个互不相关的监听器。

从Node事件发射器继承

如果你对Node的事件发射器模式感兴趣,并打算用到自己的应用程序里,你可以通过继承EventEmitter来创建一个伪类:

代码如下:

util = require('util');

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

// 这是MyClass的构造函数:

var MyClass = function() {

}

util.inherits(MyClass, EventEmitter);

注意:util.inherits建立了MyClass的原形链,让你的MyClass实例可以使用EventEmitter的原形方法。

发射事件

通过继承自EventEmitter,MyClass可以像这样发射事件了:

代码如下:

MyClass.prototype.someMethod = function() {

this.emit("custom event", "argument 1", "argument 2");

};

上面的代码,当someMethond方法被MyClass的实例调用时,就会发射一个叫“cuteom event”的事件,这个事件还会发射两个字符串作为数据:“argument 1”和“argument 2”,它们将会作为参数传递给事件监听器。

MyClass实例的客户端可以像这样监听“custom event”事件:

代码如下:

var myInstance = new MyClass();

myInstance.on('custom event', function(str1, str2) {

console.log('got a custom event with the str1 %s and str2 %s!', str1, str2);

});

再比如,你可以这样创建一个每秒发射一次“tick”事件的Ticker类:

代码如下:

var util = require('util'),

EventEmitter = require('events').EventEmitter;

var Ticker = function() {

var self = this;

setInterval(function() {

self.emit('tick');

}, 1000);

};

util.inherits(Ticker, EventEmitter);

用Ticker类的客户端可以展示如何使用Ticker类和监听“tick”事件,

代码如下:

var ticker = new Ticker();

ticker.on("tick", function() {

console.log("tick");

});

小结

事件发射器模式是种可重入模式(recurrent pattern),可以用它将事件发射器对象从一组特定事件的代码中解耦合。

可以用event_emitter.on()来为特定类型的事件注册监听器,并用event_emitter.removeListener()来取消注册。

还可以通过继承EventEmitter和简单的使用.emit()函数来创建自己的事件发射器。

(0)

相关推荐

  • 深入理解Node.js中通用基础设计模式

    谈到设计模式,你可能会想到 singletons, observers(观察者) 或 factories(工厂方法).本文不并专门探讨他们.只是探讨Node.JS一些基础模式的实现,像依赖注入或中间件. 什么是设计模式? 设计模式是用来解决一般的,普遍发生的问题,且可重复使用的解决方案. Singletons (单例) Singletons模式限制了"类",只有一个实例.在Node.js的创建单例是非常简单的,比如下面这个require. //area.js var PI = Math

  • node.js chat程序如何实现Ajax long-polling长链接刷新模式

    废话不多说,开始今天的主题.纵观这个程序,感觉它的最可贵之处,在于展示了,如何用nodejs实现长链接模式的刷新技术. (这个程序不详细介绍,重点讲解这个功能) Client.js 首先看一段核心代码: 复制代码 代码如下: function longPoll (data) { //....此处省略**行 $.ajax({ cache: false , type: "GET" , url: "/recv" , dataType: "json" ,

  • 剖析Node.js异步编程中的回调与代码设计模式

    NodeJS 最大的卖点--事件机制和异步 IO,对开发者并不是透明的.开发者需要按异步方式编写代码才用得上这个卖点,而这一点也遭到了一些 NodeJS 反对者的抨击.但不管怎样,异步编程确实是 NodeJS 最大的特点,没有掌握异步编程就不能说是真正学会了 NodeJS.本章将介绍与异步编程相关的各种知识. 在代码中,异步编程的直接体现就是回调.异步编程依托于回调来实现,但不能说使用了回调后程序就异步化了.我们首先可以看看以下代码. function heavyCompute(n, callb

  • Node.js 中的 fs 模块与Path模块方法详解

    概述: 文件系统模块是一个简单包装的标准 POSIX 文件 I/O 操作方法集.可以通过调用 require("fs") 来获取该模块.文件系统模块中的所有方法均有异步和同步版本. 文件系统模块中的异步方法需要一个完成时的回调函数作为最后一个传入形参. 回调函数的构成由调用的异步方法所决定,通常情况下回调函数的第一个形参为返回的错误信息. 如果异步操作执行正确并返回,该错误形参则为null或者undefined.如果使用的是同步版本的操作方法,一旦出现错误,会以通常的抛出错误的形式返回

  • node.js中fs.stat与fs.fstat的区别详解

    前言 fs.stat和fs.fstat他们的方法功能是一样的,都是获取文件的状态信息,本文主要介绍的是关于node.js中fs.stat与fs.fstat区别的相关内容,分享出来供大家参考学习,下面来看看详细的介绍: fs.stat用法: fs.stat('./aa.js', function(err, stats) { if (err) { throw err; } console.log(stats); }); fs.fstat用法: fs.open('./aa.js', 'a', func

  • node.js中fs文件系统模块的使用方法实例详解

    本文实例讲述了node.js中fs文件系统模块的使用方法.分享给大家供大家参考,具体如下: node.js中为我们提供了fs文件系统模块,实现对文件或目录的创建,修改和删除等操作. fs模块中,所有的方法分为同步和异步两种实现. 有 sync 后缀的方法为同步方法,没有 sync 后缀的方法为异步方法. 一.文件的整个读取 const fs = require('fs'); //参数一表示读取的文件 //参数二表示读取的配置,{encoding:'null', flag:'r'} //encod

  • Node.js中的模块化,npm包管理器详解

    目录 模块化的基本概念 什么是模块化 模块化拆分的好处 Node.js中的模块化 Node.js中模块的分类 加载模块 模块作用域 向外共享模块作用域中的成员 module对象 exports对象 npm与包 包 如何下载包 在项目中安装包的命令 解决下包速度慢的问题 为什么下包速度慢 解决方法一--淘宝NPM镜像服务器 解决方法二--切换npm的下包镜像源 解决方法三--nrm 总结 模块化的基本概念 什么是模块化 模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程.对于整个

  • Node.js中的不安全跳转如何防御详解

    导语: 早年在浏览器大战期间,有远见的Chrome认为要运行现代Web应用,浏览器必须有一个性能非常强劲的Java引擎,于是Google自己开发了一个高性能的开源的Java引擎,名字叫V8.在2009年,Ryan正式推出了基于Java语言和V8引擎的开源Web服务器项目,命名为Node.js. 对于任何web开发人员来说,不安全或未经验证的重定向都是重要的安全考虑因素.Express为重定向提供了本地支持,使它们易于实现和使用.Express是一种保持最低程度规模的灵活Node.js Web应用

  • Node.js中环境变量process.env的一些事详解

    前言 最近这两天在和运维GG搞部署项目的事儿.碰到一个问题就是,咱们的dev,uat,product环境的问题. 因为是前后端分离,所以在开发和部署的过程中会有对后端接口的域名的切换问题.折腾了一下午,查询了各种资料这才把这Node环境变量process.env给弄明白. 下面这就做个问题解决的记录.希望能对这个不明白的人有所帮助.话不多说了,来一起看看详细的介绍吧. Node环境变量 首先,咱们在做react.vue的单页应用开发的时候,相信大家对配置文件里的process.env并不眼生.

  • node.js中express中间件body-parser的介绍与用法详解

    前言 Node中的核心模块分两类:一类是自带的核心模块,如http.tcp等,第二类是第三方核心模块,express就是与http对应的第三方核心模块,用于处理http请求.express在3.0版本中自带有很多中间件,但是在express 4.0以后,就将除static(静态文件处理)以外的其他中间件分离出来了:在4.0以后需要使用中间件时,就需要单独安装好相应的中间件以后调用,以下3.0与4.0中间件的中间件区别(3.0是内置中间件属性名,4.0是需要安装的中间件名称): Express 3

  • Node.js中使用Buffer编码、解码二进制数据详解

    JavaScript很擅长处理字符串,但是因为它最初的设计是用来处理HTML文档,因此它并不太擅长处理二进制数据.JavaScript没有byte类型,没有结构化的类型(structured types),甚至没有字节数组,只有数字和字符串.(原文:JavaScript doesn't have a byte type - it just has numbers - or structured types, or http://skylitecellars.com/ even byte arra

  • Node.js高级编程cluster环境及源码调试详解

    目录 前言 准备调试环境 编译 Node.js 准备 IDE 环境 Cluster 源码调试 SharedHandle RoundRobinHandle 为什么端口不冲突 SO_REUSEADDR 补充 SharedHandle 和 RoundRobinHandle 两种模式的对比 前言 日常工作中,对 Node.js 的使用都比较粗浅,趁未羊之际,来学点稍微高级的,那就先从 cluster 开始吧. 尼古拉斯张三说过,“带着问题去学习是一个比较好的方法”,所以我们也来试一试. 当初使用 clu

  • Node.js基础入门之缓存区与文件操作详解

    目录 缓存区 1. 什么是缓存区? 2. 创建指定长度的缓存区 3. 通过数组创建缓存区 4. 通过字符串创建缓存区 5. 读写缓存区 6. 复制缓存区 文件操作 1. 异步直接读取 2. 同步直接读取 3. 流式读取 4. 写入文件 5. 流式写入文件 6. 读取文件信息 7. 删除文件 8. 管道 9. 链式流 经过前面三天的学习,Node.js的基础知识已逐渐掌握,今天继续学习缓存区和文件操作,并稍加整理加以分享,如有不足之处,还请指正. 缓存区 1. 什么是缓存区? JavaScript

随机推荐