Node.js基础设计模式 — 回调模式/观察者模式

回调模式:

引言

回调是Node.js独特的编程风格印记。回调是被调用来传播操作结果的函数,这正是我们在处理异步操作时所需要的,它们会替代总是同步执行的return指令。

JavaScript是一种很好的表示回调的语言,因为如你所见,函数首先是类对象,可以很容易的分配给变量,作为参数传递,从另一个函数调用返回或储存到数据结构。

例如,在以往的语言特性中,我们总会认为return语句是一个函数的结束:

function add(a, b) {
    return a + b
}
console.log(add(2, 3)) //=> 5
复制代码

但是JavaScript可以将函数作为参数,形成一个作用域,而不仅仅是在一个函数中调用另一个函数那么简单

//CPS
function add(a, b, fn) {
    fn(a + b)
    return undefined //默认返回undefined
}
function csl(value) {
    console.log(value)
}
add(2, 3, csl)  //=> 5
复制代码

另一个实现回调的理想构造的闭包。使用闭包,我们实际上可以引用创建函数的环境,可以始终维持异步操作被请求时的上下文,不用关系他的回调被调用的实现或地点

CPS(Continuation Passing Style)

在JavaScript中,回调是一个作为参数传递给另一个函数的函数,当操作完成时将调用该结果。在函数编程中,这种传播结果的方式被成为CPS。这是一个通用的概念,它并不总是与异步操作相关联,例如上面的一段代码就是同步CPS,与之相对的是异步CPS

为了引入异步的CPS,我们再将上面的同步CPS函数稍微改造一下:

function add(a, b, fn) {
    fn(a + b)
}
console.log('before')
add(2, 3, (value) => console.log('value: ' + value))
console.log('after')

/*输出:
	before
	value: 5
	after
*/
复制代码

事实上,如上的代码设计并没有特别的地方,现在考虑异步CPS的情况:

function add(a, b, fn) {
    setTimeout(() => {
        fn(a + b)
    }, 0)
}
console.log('before')
add(2, 3, (value) => console.log('value: ' + value))
console.log('after')
/*输出:
	before
	after
	value: 5
*/
复制代码

由于setTimeout是异步函数,它会等主线程执行完了再执行,因此是最后输出,异步CPS的价值就体现出来了

  • 作为回调函数,可以始终维持异步操作被请求时的上下文:
  • 作为异步函数,可以不阻塞地继续执行代码

在这里插入图片描述
实际上,CPS的影响和作用并非那么简单,随着我们对异步编程的逐渐深入,日后我们将会看到它的强大

非CPS回调

并非把函数当参数传进了另一个函数中就成为了GPS,在有些情况下,回调参数的存在可能会让我们认为这个函数是异步的或使用CPS,实际上并不总是这个样子

console.log([2, 4, 6, 8].map(it => it + 2)) //=> [ 4, 6, 8, 10 ]
复制代码

这里的回调函数只是用来遍历数组元素的,CPS的一个重要特征是

  • 传递操作结果

是否使用回调通常会在API文档中清除地说明

观察者模式:

引言:

在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的方法介绍,官网有详细描述nodejs.cn/api/events.…

创建和使用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函数可以使多个监听器接收相同的通知
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享