Node.js基础设计模式 --- 观察者模式

引言:

在Node.js中使用的另一个重要和基本的模式是观察者模式,观察者模式也是平台的支柱之一,并且是使用node核心和用户模块的先决条件

观察者定义了一个理想的解决方案,用于构建Node.js的反应特性,并且是回调的完美补充。下面给出一个正式的定义:

观察者模式定义了一个对象(称为主体),当它的反应状态发生改变时,它可以通知一组观察者(或者监听者)

与回调模式的主要区别是,主体实际上可以通知多个观察者,而传统的CPS回调通常将其结果传播给一个监听者,即回调

EventEmitter类

在传统的面向对象编程中,观察者模式需要接口、实体类和层次结构,而在Node.js中,一切都变得简单了,观察者模式已经内置在内核中,并且可以通过EventEmitter类获得。EventEmitter类允许我们将一个或多个函数注册成监听器,当一个特定的时间类型被触发时,它将被调用:


 ┌───────────────────────────────────────────────────────────┐
 │                                                           │
 │                                        ┌────────────┐	 │
 │										  │			   │	 │
 │                                        │            │	 │
 │										  │			   │	 │
 │                                    ┌──►│  Listener  │	 │
 │									  │   │            │	 │
 │                   ┌─┬────────────┐ │   │            │	 │
 │				     │ │ 			│ │   │            │     │
 │     ┌─────────────┼─┤            │ │   └────────────┘     │
 │     │             │ │  EventA    ├─┤                      │
 │     │             │ │            │ │   ┌────────────┐     │
 │     │             └─┼────────────┘ └──►│            │     │
 │     │  EventEmitter │                  │  Listener  │     │
 │     │             ┌─┼────────────┐ ┌──►│            │     │
 │     │             │ │            │ │   └────────────┘     │
 │     │             │ │  EventB    ├─┤                      │
 │     └─────────────┼─┤            │ │   ┌────────────┐	 │
 │					 │ │			│ │   │            │     │
 │                   └─┴────────────┘ └───►            │     │
 │										  │ 		   │     │
 │                                        │  Listener  │	 │
 │										  │			   │     │
 │                                        │            │	 │
 │										  │			   │	 │
 │                                        └────────────┘     │
 │                                                           │
 └───────────────────────────────────────────────────────────┘

EventEmitter类可以从node提供的events模块中获得

let EventEmitter = require('events').EventEmitter

这里给出几个常用的方法

  • on
  • once
  • emit
  • removeListner

关于EventEmitter的方法介绍,官网有详细描述http://nodejs.cn/api/events.html

创建和使用EventEmitter

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'   在监听之后触发才有效果

选择观察者模式(EventEmitter)还是回调模式(callback)?

在定义异步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是最佳的选择

最后

  • 使用回调的API可以仅通知特定的回调,而使用EventEmitter函数可以使多个监听器接收相同的通知

你可能感兴趣的:(Node.js)