【若川视野 x 源码共读】第8期 | mitt、tiny-emitter 发布订阅
前言
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
什么是发布订阅
发布订阅模式,对于前端开发来说,并不陌生,在我们刚接触前端开发的时候,我们就已经开始接触到其雏形了,addEventListener
和removeEventListener
,给某给DOM元素绑定事件。Vue
中的$on
和$emit
方法,也是采用了发布订阅模式,我们一起来探索下其实现方法。
发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。
订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。
探索发布订阅
从字面意思上看,我们知道该模式主要分为两个部分,即发布
和订阅
,我们通过on
来进行事件的订阅,通过emit
来进行事件的发布,同时我们还需要一个调度中心event
来存储订阅。
我们通过Mitt
类来实现,我们先来定义一下其雏形。
// 定义Mitt类
class Mitt{
// 调度中心
event = {};
// 订阅
on() {};
// 发布
emit() {};
}
订阅
接下来,我们先来完成事件订阅吧,我们来回忆下订阅过程
// 订阅使用方法
on("save", () => {console.log("save")})
我们知道,订阅需要两个参数事件名name
和事件callback
,我们很容易实现订阅
// 订阅实现方法
on(name, callback) {
this.event[name] = callback;
}
我们可能会订阅多次,这时我们会发现一个问题,上述方法并不满足,后续的订阅会覆盖掉之前的订阅,所以我们需要通过一个数组来存储所有的订阅,我们改进一下。
// 订阅实现方法
on(name, callback) {
// 判断是否存在当前订阅,若不存在,初始化为[]
const event = this.event[name] || (this.event[name] = []);
// 存储当前订阅
event.push(callback);
}
我们来测试一下订阅
const mitt = new Mitt();
mitt.on("save", () => console.log("save1"));
mitt.on("save", () => console.log("save2"));
mitt.on("del", () => console.log("del"))
console.log(mitt.event);
/*
{
save: [
() => console.log("save1"),
() => console.log("save1")
],
del: [
() => console.log("del")
]
}
*/
发布
实现完订阅,我们先来完成事件发布吧,事件发布过程很简单
// 发布使用方法
emit("save")
我们只需要通过emit
发布事件name
// 发布实现方法
emit(name) {
// 获取订阅的事件集合
const events = this.event[name];
// 如果存在,遍历执行每一个订阅
if(events) {
events.forEach(event => {
event();
})
}
};
我们来测试一下发布
mitt.emit("save");
// save1 save2
自此我们便简单实现了订阅发布,相对来说还是比较简单的,将复杂的事情简单化,有助于我们去理解。
我们除了订阅发布外,我们知道,还存在取消订阅
取消订阅
取消订阅的过程,就将之前的订阅删除,通过off
来进行取消订阅
// 取消订阅实现方法
off(name) {
// 如果传了特定的订阅,则清除其相关的订阅,否则清除所有的订阅
if(name) {
this.event[name] = [];
} else {
this.event = {};
}
}
我们来测试一下取消发布
mitt.emit("del"); // del
mitt.off("del");
mitt.emit("del"); // 未触发订阅
mitt源码解析
通过闭包来使用
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);
})
}
debugger;
// 如果存在监听*的情况,则会将事件全部触发一次
handlers = all.get('*');
if (handlers) {
handlers.slice().map((handler) => {
handler(type, evt);
});
}
}
}
}
看源码后,比较迷惑的是*
订阅所有的事件,实际触发的还是订阅*
的事件。
tiny-emitter源码解析
通过实例化E
类来使用
function E () {}
E.prototype = {
// 订阅
on: function (name, callback, ctx) {
// 创建变量e,即事件调度中心,通过普通对象存储
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
// 调用on订阅事件
return this.on(name, listener, ctx);
},
// 发布
emit: function (name) {
// 获取callback函数的运行参数
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]);
}
}
// 如果存在其他订阅事件,则修改原存储的事件,否则直接删掉原存储事件
(liveEvents.length)
? e[name] = liveEvents
: delete e[name];
return this;
}
};
总结
理解了发布订阅模式的概念和实现原理,自己对于源码的阅读也更加轻松。
在取消订阅时,之前也了解过,通过订阅事件生成唯一id来进行定向取消订阅,感兴趣的可以自己尝试一下。
转载自:https://juejin.cn/post/7182583235786833977