回调模式:
引言
回调是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函数可以使多个监听器接收相同的通知