基于mitt、tiny-emitter 的源码发布/订阅模式解析
- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第8期,链接:juejin.cn/post/708498…
什么是发布订阅模式
此模式分为发布者和订阅者两个概念,发布者收集订阅者的需求,然后在某个时刻告知订阅者。 发布订阅模式中,有三种主体:1. publisher (发布者) 2. subscriber (订阅者)3. broker(经纪人或者叫调度中心)。还有一个概念叫做topic(话题或者叫做订阅的主题)
发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。 订阅者(Subscriber)把自己想订阅的事件(topic)注册(Subscribe)到调度中心(Event Channel或broker),当发布者(Publisher)发布该事件(topic)到调度中心,也就是该事件触发时,由调度中心统一调度订阅者注册到调度中心的处理代码。
和发布订阅模式最容易混淆的是观察者模式,如果不是特别清楚的话,建议看一下二者的对比,加深一下理解。 【JS设计模式】观察者模式VS发布订阅模式以及发布订阅模式
从上面了解了什么是发布订阅模式,那么它的作用就很明显的知道了,就是用于监听和触发事件执行不同的操作。
mitt源码解析
先看一下mitt这个库是怎么使用的:
import mitt from 'mitt'
const emitter = mitt()
emitter.on('foo', e => console.log('foo', e) )
emitter.on('*', (type, e) => console.log(type, e) )
emitter.emit('foo', { a: 'b' })
emitter.all.clear()
function onFoo() {}
emitter.on('foo', onFoo)
emitter.off('foo', onFoo)
on是用来注册监听的,emit是触发监听,off是移除监听,all 参数用来存储事件类型和事件处理函数的映射Map,如果不传,就 new Map()赋值给 all。
代码是ts的看起来我认为比较难受,所以我改成了js的:
export default function mitt(all) {
all = all || new Map();
return {
all,
on (type, handler) {
// 查找 map中是否有这个key
const handlers = all.get(type);
// 存在就直接push
if (handlers) {
handlers.push(handler);
} else {
// 不存在就创建 value是一个数组
all.set(type, [handler]);
}
},
off (type, handler) {
const handlers = all.get(type);
if (handlers) {
if (handler) {
// 删除指定的 handler处理函数, 找到了 idx >>> 0 就是idx对应的索引
// 没找到 -1 变为 4294967295,原数组不会改变
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
} else {
// 如果没有传对应的handler,则把此type的全部事件全部清除
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);
});
}
}
}
}
这一块代码我改造完成后最疑惑的就是handlers.splice(handlers.indexOf(handler) >>> 0, 1);
这里为什么要使用右移运算符,我试过之后的大概想法是,如果出现的是-1这个值,如果不进行处理的话,那么就会删除数组最后一个函数,这个是有问题的,所以对它进行处理,这样就可以避免删除不该删除的函数,如果不是这个意思,麻烦大哥们在评论区告诉我原因。当然还有一个问题就是关于all这个变量,一定要设置为map类型,因为下面取值的时候都用的是set与get,否则将会报错。
tiny-emitter源码解析
tiny-emitter的原理和mitt其实是一样的,但是功能上有些许差异,并且实现上也有一些区别,大致的结构如下:
function E () {
}
E.prototype = {
on: function (name, callback, ctx) {},
once: function (name, callback, ctx) {},
emit: function (name) {},
off: function (name, callback) {}
};
module.exports = E;
module.exports.TinyEmitter = E;
tiny-emitter是针对原型链上或者说是基于class实现的,这种方式是原型模式,而mitt是基于返回一个对象的方式实现的,这种方式是工厂模式。
分别来看一下这些函数吧:
// on函数
on: function (name, callback, ctx) {
// 创建变量e 指向this的属性 不存在则创建
var e = this.e || (this.e = {});
// 将对应的函数存放到对应的事件中,也是一个数组的形式
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
});
return this;
},
// once函数
once: function (name, callback, ctx) {
var self = this;
// 实现once 的大前提是我们需要传入命名函数,存在引用关系可以让我们移除
function listener () {
// 移除对应的函数
self.off(name, listener);
// 调用一次 once传入的callback
callback.apply(ctx, arguments);
};
// 存放引用,用于移除
listener._ = callback
// 调用on监听事件
return this.on(name, listener, ctx);
},
// emit函数
emit: function (name) {
// 取arguments除第一个参数后面的参数
var data = [].slice.call(arguments, 1);
// 获取对应name的执行函数数组
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
var i = 0;
var len = evtArr.length;
for (i; i < len; i++) {
// 依次将每个值(对象) 的fn属性的事件 通过apply调用 并传入 this指向 以及data剩余参数
evtArr[i].fn.apply(evtArr[i].ctx, data);
}
return this;
},
// off函数
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]);
}
}
// 如果liveEvents 存在内容则 给 e[name]重新赋值, 否则直接移除 这个name属性
(liveEvents.length)
? e[name] = liveEvents
: delete e[name];
return this;
}
总结
看了上面两个库对于发布/订阅的不同实现其实还是学到了很多东西,包括对于原型的使用,对于发布/订阅概念的理解,还有上面对于同一件事情实现的不同,对于我开阔眼界有一些帮助。
转载自:https://juejin.cn/post/7172124277985001486