观察者模式(observer)普遍的应用于javascript语言中,浏览器事件(如鼠标单击click,键盘事件keyDown)都是该模式的例子。设计这种模式背后的主要缘由是促进造成低耦合,在这种模式中不是简单的对象调用对象,而是一个对象“订阅”另外一个对象的某个活动,当对象的活动状态发生了改变,就去通知订阅者,而订阅者也称为观察者。javascript
生活中就像是去报社订报纸,你喜欢读什么报就去报社去交钱订阅,当发布了新报纸的时候,报社会向全部订阅了报纸的每个人发送一份,订阅者就能够接收到。java
咱们能够利用这个例子来使用javascript来模拟一下。假设有一个发布者Jack,它天天出版报纸杂志,订阅者Tom将被通知任什么时候候发生的新闻。node
Jack要有一个subscribers属性,它是一个数组类型,订阅的行为将会按顺序存放在这个数组中,而通知意味着调用订阅者对象的某个方法。所以,当用户Tom订阅信息的时候,该订阅者要向Jack的subscribe()提供他的一个方法。固然也能够退订,我不想再看报纸了,就调用unsubscribe()取消订阅。数组
一个简单的观察者模式应有如下成员:浏览器
这个模式中还须要一个type参数,用于区分订阅的类型,若有的人订阅的是娱乐新闻,有的人订阅的是体育杂志,使用此属性来标记。函数
咱们使用简单的代码来实现它:ui
var Jack = {
subscribers: {
'any': []
},
//添加订阅
subscribe: function (type = 'any', fn) {
if (!this.subscribers[type]) {
this.subscribers[type] = [];
}
this.subscribers[type].push(fn); //将订阅方法保存在数组里
},
//退订
unsubscribe: function (type = 'any', fn) {
this.subscribers[type] =
this.subscribers[type].filter(function (item) {
return item !== fn;
}); //将退订的方法从数组中移除
},
//发布订阅
publish: function (type = 'any', ...args) {
this.subscribers[type].forEach(function (item) {
item(...args); //根据不一样的类型调用相应的方法
});
}
};
复制代码
以上就是一个最简单的观察者模式的实现,能够看到代码很是的简单,核心原理就是将订阅的方法按分类存在一个数组中,当发布时取出执行便可。this
下面使用Tom来订报:spa
var Tom = {
readNews: function (info) {
console.log(info);
}
};
//Tom订阅Jack的报纸
Jack.subscribe('娱乐', Tom.readNews);
Jack.subscribe('体育', Tom.readNews);
//Tom 退订娱乐新闻:
Jack.unsubscribe('娱乐', Tom.readNews);
//发布新报纸:
Jack.publish('娱乐', 'S.H.E演唱会惊喜登台')
Jack.publish('体育', '欧国联-意大利0-1客负葡萄牙');
复制代码
运行结果:prototype
欧国联-意大利0-1客负葡萄牙
复制代码
能够看到观察者模式将两个对象的关系变得十分松散,当不须要订阅关系的时候删掉订阅的语句便可。那么在实际应用中有哪些地方使用了这个模式呢?
node.js的events是一个使用率很高的模块,其它原生node.js模块都是基于它来完成的,好比流、HTTP等,咱们能够手写一版events的核心代码,看看观察者模式的实际应用。
events模块的功能就是一个事件绑定,全部继承自它的实例都具有事件处理的能力。首先它是一个类,咱们写出它的基本结构:
function EventEmitter() {
//私有属性,保存订阅方法
this._events = {};
}
//默认最大监听数
EventEmitter.defaultMaxListeners = 10;
module.exports = EventEmitter;
复制代码
下面咱们一个个将events的核心方法实现。
首先是on方法,该方法用于订阅事件,在旧版本的node.js中是addListener方法,它们是同一个函数:
EventEmitter.prototype.on =
EventEmitter.prototype.addListener = function (type, listener, flag) {
//保证存在实例属性
if (!this._events) this._events = Object.create(null);
if (this._events[type]) {
if (flag) {//从头部插入
this._events[type].unshift(listener);
} else {
this._events[type].push(listener);
}
} else {
this._events[type] = [listener];
}
//绑定事件,触发newListener
if (type !== 'newListener') {
this.emit('newListener', type);
}
};
复制代码
由于有其它子类须要继承自EventEmitter,所以要判断子类是否存在_event属性,这样作是为了保证子类必须存在此实例属性。而flag标记是一个订阅方法的插入标识,若是为'true'就视为插入在数组的头部。能够看到,这就是观察者模式的订阅方法实现。
EventEmitter.prototype.emit = function (type, ...args) {
if (this._events[type]) {
this._events[type].forEach(fn => fn.call(this, ...args));
}
};
复制代码
emit方法就是将订阅方法取出执行,使用call方法来修正this的指向,使其指向子类的实例。
EventEmitter.prototype.once = function (type, listener) {
let _this = this;
//中间函数,在调用完以后当即删除订阅
function only() {
listener();
_this.removeListener(type, only);
}
//origin保存原回调的引用,用于remove时的判断
only.origin = listener;
this.on(type, only);
};
复制代码
once方法很是有趣,它的功能是将事件订阅“一次”,当这个事件触发过就不会再次触发了。其原理是将订阅的方法再包裹一层函数,在执行后将此函数移除便可。
EventEmitter.prototype.off =
EventEmitter.prototype.removeListener = function (type, listener) {
if (this._events[type]) {
//过滤掉退订的方法,从数组中移除
this._events[type] =
this._events[type].filter(fn => {
return fn !== listener && fn.origin !== listener
});
}
};
复制代码
off方法即为退订,原理同观察者模式同样,将订阅方法从数组中移除便可。
EventEmitter.prototype.prependListener = function (type, listener) {
this.on(type, listener, true);
};
复制代码
此方法没必要多说了,调用on方法将标记传为true(插入订阅方法在头部)便可。
以上,就将EventEmitter类的核心方法实现了。
经过建立“可观察的”对象,当发生一个感兴趣的事件时可将该事件通告给全部观察者,从而造成松散的耦合。
部分实例参考《JavaScript模式》做者:Stoyan Stefanov