本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
本期共读的两个模块mitt和tiny-emitter是对观察者模式的实现。
什么是观察者模式
定义一对多的依赖关系,当一个对象发生改变,所有依赖她的对象都得到通知。
优势 1.可应用异步编程,替代回调。2.解耦,不必显示调用另一对象某个接口
弊端 模块之间的联系被隐藏了。
观察者模式有一个别名是发布-订阅模式,但经过发展发布订阅模式已经独立于观察者模式形成新的设计模式,两者的区别在于发布订阅模式具备调度中心
mitt源码分析
使用方式如下:
import mitt from 'mitt'
const emitter = mitt()
// listen to an event
emitter.on('foo', e => console.log('foo', e) )
// listen to all events
emitter.on('*', (type, e) => console.log(type, e) )
// fire an event
emitter.emit('foo', { a: 'b' })
// clearing all events
emitter.all.clear()
// working with handler references:
function onFoo() {}
emitter.on('foo', onFoo) // listen
emitter.off('foo', onFoo) // unlisten
源码比较简单,核心代码如下:
export default function mitt(all){
all = all || new Map();
return {
all,
on(type, handler) {
const handlers = all.get(type);
if (handlers) {
handlers.push(handler);
}
else {
all.set(type, [handler]);
}
},
off(type, handler){
const handlers = all.get(type);
if (handlers) {
if (handler){
handlers.splice(handlers.indexOf(handler)>>>0, 1);
}
else {
all.set(type, []);
}
}
},
emit(type, evt){
let handlers = all.get(type);
if (handlers) {
handlers
.slice()
.map((handler) => {
handler(evt);
});
}
handlers = all.get('*');
if (handlers) {
handlers
.slice()
.map((handler) => {
handler(type, evt);
});
}
}
};
}
代码解读亮点
- 采用Map进行关系保存,而不是之前的Object。
- 在off的Api中使用了无符号右移。在以往的代码中我们可能会找取对应索引,判断是否存在,存在才移除。但使用无符号右移,若不存在的话则会得到一个极大正数,那么使用splice也并不会删除对应的监听。
- 监听所有事件的监听器,在on的时候type为*,则监听所有事件。emit的时候除了触发自身当前type的事件,也会触发所有事件的。
tiny-emitter源码分析
源码比较简单,如下:
function E () {
// Keep this empty so it's easier to inherit from
// (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
}
E.prototype = {
on: function (name, callback, ctx) {
var e = this.e || (this.e = {});
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
});
return this;
},
once: function (name, callback, ctx) {
var self = this;
function listener () {
self.off(name, listener);
callback.apply(ctx, arguments);
};
listener._ = callback
return this.on(name, listener, ctx);
},
emit: function (name) {
var data = [].slice.call(arguments, 1);
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
var i = 0;
var len = evtArr.length;
for (i; i < len; i++) {
evtArr[i].fn.apply(evtArr[i].ctx, data);
}
return this;
},
off: function (name, callback) {
var e = this.e || (this.e = {});
var evts = e[name];
var liveEvents = [];
if (evts && callback) {
for (var i = 0, len = evts.length; i < len; i++) {
if (evts[i].fn !== callback && evts[i].fn._ !== callback)
liveEvents.push(evts[i]);
}
}
// Remove event from queue to prevent memory leak
// Suggested by https://github.com/lazd
// Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
(liveEvents.length)
? e[name] = liveEvents
: delete e[name];
return this;
}
};
module.exports = E;
module.exports.TinyEmitter = E;
代码解读亮点
- 相比mitt多了once方法,解决思路是重新创建回调命名为listen,在执行真正的cb前调用off,并将原函数放入listen的_属性中。同时在off中检查这个_属性。
- 主体方法on、off、emit函数最后都返回了this,实现链式调用
- 一些判断赋值简写,例如
(e[name] || (e[name] = [])).push
以 Class 方式重写 tiny-emitter
class EventEmitter{
// 用来存放注册的事件与回调
constructor(){
this._events = {};
}
on(eventName, callback){
// 由于一个事件可能注册多个回调函数,所以使用数组来存储事件队列
this._events[eventName] = [...(this._events[eventName] || []), callback];
}
emit(eventName, ...args){
const callbacks = this._events[eventName] || [];
callbacks.forEach(callback => callback(...args));
}
off(eventName, callback){
if(callback){
this._events[eventName] = this._events[eventName].filter(fn => fn != callback && fn.initialCallback != callback)
}else{
this._events[eventName] = []
}
}
once(eventName, callback){
// 由于需要在回调函数执行后,取消订阅当前事件,所以需要对传入的回调函数做一层包装,然后绑定包装后的函数
const one = (...args)=>{
// 执行回调函数
callback(...args)
// 取消订阅当前事件
this.off(eventName, one)
}
// 由于:我们订阅事件的时候,修改了原回调函数的引用,所以,用户触发 off 的时候不能找到对应的回调函数
// 所以,我们需要在当前函数与用户传入的回调函数做一个绑定,我们通过自定义属性来实现
one.initialCallback = callback;
this.on(eventName, one)
}
}
总结与收获
经过这两个库的通读,对观察者模式理解更深刻了。同时MITT是TS编写的,通读过程掌握TS一些关键字的使用。