node设计模式第二章-回调/模块/观察者模式
回调
闭包的概念: 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。
闭包可以模拟私有方法。
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
cps(continuation passing style)
回调,是一个作为参数传递给另一个函数的函数,当操作完成时将调用该函数,在函数编程中,这种传播方式称为cps, 这是一个通用概念,并不总是与异步操作关联
cps即表示通过将结果传递给另一个函数(回调)而使结果传播,而不是直接返回给调用者。
同步函数会发生阻塞,直到它完成操作,异步函数会立即返回,并且在事件循环的后续周期将结果传递给处理程序(回调)
回调约定
回调函数置尾,当一个函数在输入中接受一个回调时,它必须作为最后一个参数传递
暴露错误优先,cps函数产生的任何错误总是作为回调的第一个参数传递
传播错误,在异步cps中,可以将错误简单传递到链中的下一个回调来进行正确的错误传播
未捕获异常,在异步回调中抛出异常将导致异常跳转到事件循环,并且不会传播到下一个回调,在nodejs中这是一个不可恢复的状态,应用程序将简单地关闭并输出错误到stderr接口。
模块
模块用于构造程序时,隐藏信息保护一些函数等。CommonJS 是一个旨在标准化JavaScript生态系统的团体,其中最受欢迎的提案之一称为 CommonJS模块, nodejs在这个规范之上构建了模块系统。
书中写了一个简单代码来能让人快速理解其中模块的构造
function loadModule(filename, module, require){
function loadModule(filename, module, require){
const wrappedSrc = `(function(module, exports, require){
${fs.readFileSync(filename, 'utf8')}
})(module, module.exports, require);` ;
eval(wrappedSrc);
}
模块的源代码基本上被包装到一个函数中,将变量列表 module exports require 传递给了模块
require加在模块的示例
const require = (moduleName) => {
console.log(`Require invoked for module: ${moduleName}`);
const id = require.resolve(moduleName); //[1] 模块名称被接受作为输入, 解析模块的完整路径,称之为id, 由 require.resolve 完成,它实现了一个特定的解析算法
if(require.cache[id]) { //[2] 如果已经加载过模块,那应该在缓存中,所以可以直接从缓存返回
return require.cache[id].exports;
}
//module metadata
const module = { //[3] 如果模块没有加载,需要为首次加载设置运行环境,创建一个module对象,其中包含用空对象字面量初始化的 exports属性, 该属性将用于模块的代码导出任何公共API
exports: {},
id: id
};
//Update the cache
require.cache[id] = module; //[4] module 对象被缓存
//load the module
loadModule(id, module, require); //[5] 模块源代码从其文件被读取,代码被评估, 通过操作或替换 module.exports 对象来导出其公共API
//return exported variables
return module.exports; //[6] module.exports 的内容表示模块的公共API, 返回给调用者
};
require.cache = {};
require.resolve = (moduleName) => {
/* resolve a full module id from the moduleName */
};
简要概述就是require加载对应的模块之后,将其模块中的module.exports返回给require。
exports与module.exports
使用时经常看到这2个却不晓得其区别,查阅资料如下
从上面的require函数可以知道,require是将module.exports返回出去。那么exports呢
exports变量,为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。
var exports = module.exports;
造成的结果是,在对外输出模块接口时,可以向exports对象添加方法。
exports.area = function (r) {
return Math.PI * r * r;
};
exports.circumference = function (r) {
return 2 * Math.PI * r;
};
不能直接将exports变量指向一个值,因为这样等于切断了exports
与module.exports
的联系。
exports = function(x) {console.log(x)};
下面的写法也是无效的。
exports.hello = function() {
return 'hello';
};
module.exports = 'Hello world';
上面代码中,hello
函数是无法对外输出的,因为module.exports
被重新赋值了。
总结而来,exports默认指向了module.exports的引用地址,在不改变exports地址和不重新赋值module.exports的情况下,他们是等价的
require函数是同步的。
模块解析规则
解析主要分为三个主要部分,文件模块,核心模块,包模块
-
文件模块:如果moduleName以/开头,则被视为模块的绝对路径,如果以 ./ 开头,则被视为相对路径
-
核心模块:如果moduleName没有前缀/ ./ , 则算法首先尝试从nodejs核心模块中搜索
-
包模块:如果在核心模块中没找到与moduleName 匹配的,就在所在目录的 node_modules 目录下面查找,并从所在目录结构中向上导航,直至在路径上的node_modules目录下面找到了为止,否则报错。
对于文件或者包模块,单个文件和目录都可以与moduleName匹配。算法将会尝试匹配以下
.js /index.js /package.json
node_module目录实际是npm安装每个包依赖的地方。每个包都是可以有自己私有依赖。
模块缓存
每个模块仅在第一次需要时才加载和评估,后续任何require调用都将简单地返回缓存的版本,模块缓存通过 require.cache 变量公开, 因此如果需要,可以直接访问
使用缓存使得模块依赖项中可以有循环, 在某种程度上保证在给定包中需要相同模块时总是返回相同的实例。
循环依赖
循环以来可能导致模块加载不完整,应该避免循环依赖。而且其不会在编译期发现,容易引发bug
模块定义模式
最大限度的隐藏信息和API可用性,可扩展性,代码重用
命名导出
导出一个公开接口最基本的方式是使用命名导出,即把我们想导出的值赋给 exports (或者 module.exports)
导出函数
给整个 module.exports 赋值一个函数也是一种非常流行的方式。主要的优点是只导出一个功能更加清晰明了,这被社区叫作垂直模式(substack pattern)。
暴露构建器
暴露构建器专门用于暴露函数。不同的是这种模式允许我们使用构建器创建一个新的实例,但是我们可以扩展原型并伪造新的类。
//file logger.js
function Logger(name) {
this.name = name;
}
Logger.prototype.log = function(message) {
console.log(`[${this.name}] ${message}`);
};
Logger.prototype.info = function(message) {
this.log(`info: ${message}`);
};
Logger.prototype.verbose = function(message) {
this.log(`verbose: ${message}`);
};
module.exports = Logger;
//file main.js
const Logger = require('./logger');
const dbLogger = new Logger('DB');
dbLogger.info('This is an informational message');
const accessLogger = new Logger('ACCESS');
accessLogger.verbose('This is a verbose message');
如上就是常用的导出构造函数,使其类更利于拓展,增加其灵活性。在taf中其log也是这种形式的导出,便于我们封装出我们想要的类。
暴露一个实例
在常用的业务代码中service,dao层通常都是暴露一个实例,因为其中的方法都是无状态的,不需要有多个实例。
module.exports = new Logger();
观察者模式
观察者模式,常用设计模式之一,其定义了一个对象,当他的状态发生改变时,他可以通知一组观察者。
EventEmitter
观察者模式已经被构建到 node 核心中并以 EventEmitter 类暴露。EventEmitter 允许我们注册一个或多个监听器。
一些触发器的基础方法:
- on(event, listener): 这个方法允许我们来为给定的事件注册一个新的监听器
- once(event, listener): 这个方法注册一个新的监听器且只监听一次
- emit(event, [arg1], [...]): 这个方法将产生新的事件并可传入多个参数
- removeListener(event, listener): 这个方法移除事件的监听器
因为所有的方法都会返回 EventEmitter 实例所以你可以进行串联。监听函数只接受发射器上发射的参数。
我们可以发现很多与回调不同的地方,尤其是第一个参数不再是 error 对象。
示例
const EventEmitter = require('events').EventEmitter;
const fs = require('fs');
function findPattern(files, regex) {
const emitter = new EventEmitter();
files.forEach(function(file) {
fs.readFile(file, 'utf8', (err, content) => {
if(err)
return emitter.emit('error', err);
emitter.emit('fileread', file);
let match;
if(match = content.match(regex))
match.forEach(elem => emitter.emit('found', file, elem));
});
});
return emitter;
}
findPattern(
['fileA.txt', 'fileB.json'],
/hello \w+/g
)
.on('fileread', file => console.log(file + ' was read'))
.on('found', (file, match) => console.log('Matched "' + match +
'" in file ' + file))
.on('error', err => console.log('Error emitted: ' + err.message));
创建好的发射器产生三个事件:
- fileread: 文件被读取时发生
- found: 匹配找到时发生
- error: 在读取文件发生错误时触发
传递错误
EventEmitter 在回掉内出现时不会在错误发生时抛出一个异常,因为如果事件是被异步触发的那么它将在事件轮询内丢失。每次都注册一个监听 error 事件的监听器是一项最佳实践,因为 Node.js 将以一种特别的方式来处理它然后再抛出一个异常,并在没有发现相关监听器时退出程序。
观察一切对象
有时直接从 EventEmitter 类创建一个新的对象还不够,因为提供生成新事件以外的功能不切实际。我们可以拓展EventEmitter类,来实现
示例
const EventEmitter = require('events').EventEmitter;
const fs = require('fs');
class FindPattern extends EventEmitter {
constructor (regex) {
super();
this.regex = regex;
this.files = [];
}
addFile (file) {
this.files.push(file);
return this;
}
find () {
this.files.forEach( file => {
fs.readFile(file, 'utf8', (err, content) => {
if (err) {
return this.emit('error', err);
}
this.emit('fileread', file);
let match = null;
if (match = content.match(this.regex)) {
match.forEach(elem => this.emit('found', file, elem));
}
});
});
return this;
}
}
const findPatternObject = new FindPattern(/hello \w+/);
findPatternObject
.addFile('fileA.txt')
.addFile('fileB.json')
.find()
.on('found', (file, match) => console.log(`Matched "${match}"
in file ${file}`))
.on('error', err => console.log(`Error emitted ${err.message}`));
EventEmitter 与回调的选择
回调在支持不同类型的事件时会有一些限制。 实际上,我们可以传入多种类型的参数来区分事件。 但是这算不上是优雅的 API。在这种情况下 EventEmitter 更适合。
对另一个函数来说,EventEmitter 可能更适合当相同的事件可以多次发生时。回调实际上只期待被调用一次,无论操作成功与否。当然这里使用 EventEmitter 更合适。
最后,一个使用回调的接口只可以指定一个回调,而 EventEmitter 可以使用多个监听器来接收相同的事件。
观察者模式,对于业务类型多的更利于其拓展