在Node.js中使用的另一个重要和基本的模式是观察者模式,观察者模式也是平台的支柱之一,并且是使用node核心和用户模块的先决条件
观察者定义了一个理想的解决方案,用于构建Node.js的反应特性,并且是回调的完美补充。下面给出一个正式的定义:
观察者模式定义了一个对象(称为主体),当它的反应状态发生改变时,它可以通知一组观察者(或者监听者)
与回调模式的主要区别是,主体实际上可以通知多个观察者,而传统的CPS回调通常将其结果传播给一个监听者,即回调
在传统的面向对象编程中,观察者模式需要接口、实体类和层次结构,而在Node.js中,一切都变得简单了,观察者模式已经内置在内核中,并且可以通过EventEmitter类获得。EventEmitter类允许我们将一个或多个函数注册成监听器,当一个特定的时间类型被触发时,它将被调用:
┌───────────────────────────────────────────────────────────┐
│ │
│ ┌────────────┐ │
│ │ │ │
│ │ │ │
│ │ │ │
│ ┌──►│ Listener │ │
│ │ │ │ │
│ ┌─┬────────────┐ │ │ │ │
│ │ │ │ │ │ │ │
│ ┌─────────────┼─┤ │ │ └────────────┘ │
│ │ │ │ EventA ├─┤ │
│ │ │ │ │ │ ┌────────────┐ │
│ │ └─┼────────────┘ └──►│ │ │
│ │ EventEmitter │ │ Listener │ │
│ │ ┌─┼────────────┐ ┌──►│ │ │
│ │ │ │ │ │ └────────────┘ │
│ │ │ │ EventB ├─┤ │
│ └─────────────┼─┤ │ │ ┌────────────┐ │
│ │ │ │ │ │ │ │
│ └─┴────────────┘ └───► │ │
│ │ │ │
│ │ Listener │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ └────────────┘ │
│ │
└───────────────────────────────────────────────────────────┘
EventEmitter类可以从node提供的events模块中获得
let EventEmitter = require('events').EventEmitter
这里给出几个常用的方法
关于EventEmitter的方法介绍,官网有详细描述http://nodejs.cn/api/events.html
let EventEmitter = require('events').EventEmitter
let fs = require('fs')
function findPattern(files, regex) {
let emitter = new EventEmitter()
files.forEach(file => {
fs.readFile(file, 'utf8', (err, result) => {
if (err) {
return emitter.emit('error', err) //当找不到文件时,注册并触发err事件,把错误传给监听者
}
emitter.emit('fileread', file) //注册并触发fileread事件,把文件名传给监听者
let match
if (match = result.match(regex)) {
emitter.emit('find', match) //注册并触发find事件,把内容传给监听者
}
})
});
return emitter
}
findPattern(
['fileA.txt', 'fileB.json', 'df.json'],
/hello \w+/g
)
.on('fileread', file => console.log(file + ' was read ) //监听fileread事件,file接收到打开文件的文件名
.on('find', match => console.log(match + ' was find)) //监听find时间,match为正则匹配到的字符串数组
.on('error', err=> console.log(err+ ' happend)) //监听find时间,match为正则匹配到的字符串数组
有时,直接从EventEmitter类创建一个新的可观察对象是不够的,因为提供生成新事件以外的功能是不切实际的,通过核心模块util
提供的inherits函数
,我们很容易对EventEmitter实现拓展
class filePatten extends EventEmitter {
constructor(regex) {
super()
this.files = []
this.regex = regex
}
add(fileName) {
this.files.push(fileName)
return this
}
find() {
this.files.forEach(file => {
fs.readFile(file, 'utf8', (err, result) => {
if (err) {
this.emit('error', err)
}
this.emit('fileread', file)
let match
if (match = result.match(this.regex)) {
this.emit('find', match)
}
})
})
return this
}
}
let mode = new filePatten(/hello \w+/g)
mode.add('fileA.txt')
.add('fileB.json')
.find()
.on('fileread', file => console.log(file + 'has read'))
.on('find', match => console.log(match))
通过继承EventEmitter的功能,可以看到filePatten对象是如何具有一套完整的方法,而且为了保持与EventEmitter类的方法类似,我们让自定义的方法也返回自身,return this
,保持风格的一致,因为EventEmitter.prototpye.on
等方法默认也return this
关于上述代码,你需要注意一个细节,那就是.on
方法是先由于.emit()
执行的,这是由于fs.readFile
是异步函数,监听在前,触发在后,也只有这样,我们的代码才能顺利的监听到事件并执行回调函数,这是一个容易犯错误的点,接下来将给出一个错误的用例
与回调一样,事件可以同步或异步发出,这一点在 Node.js基础设计模式 — 回调模式 文中的CPS中有提及,但重要的是,绝不能在同一个EventEmitter中混合使用这两种方法
发送同步事件和发送异步事件主要的区别在于监听器注册的方式,当事件以异步方式发出时,即在EventEmitter被初始化之后,程序仍有事件注册新的监听器(前面我们讨论的都是这种情况,注册事件是在异步函数中)
相反,同步发送事件需要在EventEmitter函数开始发出任何事件之前注册所有监听器,来看一个相反的例子
let EventEmitter = require('events').EventEmitter
class syncEmit extends EventEmitter{
constructor() {
super()
this.emit('init') //同步注册并触发init事件,但此时还没有观察者监听此事件
}
}
let sync = new syncEmit().on('init',() => console.log('init success')) // 因此语句此不会输出
稍微改造一下
let EventEmitter = require('events').EventEmitter
class syncEmit extends EventEmitter{
constructor() {
super()
}
}
let syncobj= new syncEmit().on('init',() => console.log('init success'))
syncobj.emit('init') //=> 'init success' 在监听之后触发才有效果
在定义异步API时,比较常见的困难是,怎么来判断应该使用EventEmitter还是说用回调就够了。一般的原则是:当结果必须以异步方式返回时,应该使用回调;当需要对刚刚发生的事情做传达时,使用事件。
但是,除了这个简单的原则之外,由于这两个范例大部分事件效果相当,并且可以实现相同的效果,所以产生了许多混乱,例如:
function helloEvents() {
let eventEmitter = new EventEmitter()
setTimeout(() => {
eventEmitter.emit('sayhello', 'hello world')
})
}
function helloCallback(callback) {
setTimeout(() => {
callback('hello world')
})
}
两个函数helloEvents()
和helloCallback()
在功能上可以被认为是等效的。
作为第一个观察结果,我们可以说,在支持不同类型的事件时,回调有一些限制。事实上,我们仍然可以通过将类型作为回调函数的参数传递,例如:
function helloCallback(callback) {
//...
callback('init') //回调init事件
//...
callback('read') //回调read事件
//...
callback('end') //回调end事件
//...
}
function fun(type) {
switch(type) {
//在回调函数中处理
case 'init' :
//...
case 'read' :
//...
}
}
helloCallback(fun)
或者接收几个回调,通过调用来区分不同的事件:
function helloCallback(callbackInit, callbackRead, callbackEnd) {
//...
callbackInit() //回调init事件
//...
callbackRead() //回调read事件
//...
callbackEnd() //回调end事件
//...
}
helloCallback(fun1,fun2,fun3)
然而,不能认为这是一个优雅的API,在这种情况下,显然EventEmitter可以提供更好的结构和更精简的代码
优先选择EventEmitter的另一种情况是,同一个事件可能发生多次,或者根本不发生。回调函数只能被调用一次,无论操作是否成功。事实上,有一个可能重复的情况,这让我们再次考虑事件的语义特性,其更像是一个必须传达的事件,而不是一个结果。在这种情况下EventEmitter是最佳的选择
最后