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


Posted in Javascript onAugust 15, 2014

在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()函数来创建自己的事件发射器。

Javascript 相关文章推荐
jQuery的.live()和.die() 使用介绍
Sep 10 Javascript
元素绑定click点击事件方法
Jun 08 Javascript
jQuery+CSS实现一个侧滑导航菜单代码
May 09 Javascript
[js高手之路]图解javascript的原型(prototype)对象,原型链实例
Aug 28 Javascript
基于vue-video-player自定义播放器的方法
Mar 21 Javascript
微信小程序如何像vue一样在动态绑定类名
Apr 17 Javascript
简单的三步vuex入门
May 20 Javascript
微信小程序实现语音识别转文字功能及遇到的坑
Aug 02 Javascript
webpack是如何实现模块化加载的方法
Nov 06 Javascript
使用Vant完成通知栏Notify的提示操作
Nov 11 Javascript
vue中配置scss全局变量的步骤
Dec 28 Vue.js
vue 自定义的组件绑定点击事件
Apr 21 Vue.js
Node.js中使用计时器定时执行函数详解
Aug 15 #Javascript
javascript中实现兼容JAVA的hashCode算法代码分享
Aug 11 #Javascript
javascript实现锁定网页、密码解锁效果(类似系统屏幕保护效果)
Aug 15 #Javascript
javascript使用window.open提示“已经计划系统关机”的原因
Aug 15 #Javascript
Ext4.2的Ext.grid.plugin.RowExpander无法触发事件解决办法
Aug 15 #Javascript
javascript中的__defineGetter__和__defineSetter__介绍
Aug 15 #Javascript
js 判断图片是否加载完以及实现图片的预下载
Aug 14 #Javascript
You might like
PHP实现微信网页授权开发教程
2016/01/19 PHP
PHP使用正则表达式实现过滤非法字符串功能示例
2018/06/04 PHP
常用参考资料(手册)下载或者链接
2006/07/22 Javascript
使用户点击后退按钮使效三行代码
2007/07/07 Javascript
编写Js代码要注意的几条规则
2010/09/10 Javascript
jquery弹出关闭遮罩层实例
2013/08/06 Javascript
jquery获取当前日期的方法
2015/01/14 Javascript
jquery 设置style:display的方法
2015/01/29 Javascript
javascript中AJAX用法实例分析
2015/01/30 Javascript
js实现不重复导入的方法
2016/03/02 Javascript
Javascript6中字符串的四个新用法分享
2016/09/11 Javascript
AngularJS自定义指令详解(有分页插件代码)
2017/06/12 Javascript
聊聊JS动画库 Velocity.js的使用
2018/03/13 Javascript
利用chrome浏览器进行js调试并找出元素绑定的点击事件详解
2021/01/30 Javascript
用jQuery将JavaScript对象转换为querystring查询字符串的方法
2018/11/12 jQuery
vue-cli3.0+element-ui上传组件el-upload的使用
2018/12/03 Javascript
微信小程序bindinput与bindsubmit的区别实例分析
2019/04/17 Javascript
Vue实现页面添加水印功能
2019/11/09 Javascript
js实现简单的随机点名器
2020/09/17 Javascript
[01:19]2014DOTA2国际邀请赛 采访TITAN战队ohaiyo 能赢DK很幸运
2014/07/12 DOTA
[02:27]刀塔重生降临
2015/10/14 DOTA
python中partial()基础用法说明
2018/12/30 Python
解决项目pycharm能运行,在终端却无法运行的问题
2019/01/19 Python
python实现月食效果实例代码
2019/06/18 Python
Python对接六大主流数据库(只需三步)
2019/07/31 Python
如何基于python测量代码运行时间
2019/12/25 Python
Python 面向对象之类class和对象基本用法示例
2020/02/02 Python
Python3 xml.etree.ElementTree支持的XPath语法详解
2020/03/06 Python
关于pycharm 切换 python3.9 报错 ‘HTMLParser‘ object has no attribute ‘unescape‘ 的问题
2020/11/24 Python
css3 旋转按钮 使用CSS3创建一个旋转可变色按钮
2012/12/31 HTML / CSS
如何查询Oracle数据库中已经创建的索引
2013/10/11 面试题
大专学生推荐信范文
2013/11/19 职场文书
大学四年个人自我小结
2014/03/05 职场文书
广场舞大赛策划方案
2014/05/31 职场文书
纪检干部先进事迹材料
2014/08/23 职场文书
2015年高校辅导员工作总结
2015/04/20 职场文书