1. 前言
本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第25期,链接:mitt、tiny-emitter
2. mitt、tiny-emitter是什么
mitt 和 tiny-emitter 都是轻量级的库,用于在JavaScript环境中实现自定义事件的发布和订阅。
许多复杂的JavaScript应用要依赖于事件来在组件或模块之间通信。虽然浏览器自身提供了类似的功能,如DOM事件,但这些通常过于重量级,不适合在轻量级的或者没有DOM的环境(如 Node.js)中使用。此外,DOM事件也无法满足一些特殊需求,如全局事件、通配符事件等。
因此,出现了像mitt和tiny-emitter这样的轻量级库,它们解决了如何在任何JavaScript应用中轻松地创建、监听和触发自定义事件的问题。
两者都是使用了发布订阅模式,接下来解释下其原理。
3. 观察者&发布订阅
许多人说两者不是同一个模式,个人觉得,发布订阅是在观察者的基础上变化而来,没必要纠结。
下面用两个例子进行说明
3.1 观察者模式
比如老师需要通知学生明天上课,将所有学生拉到一个消息群中,老师发送消息,所有学生都收到消息。
代码实现:
/**
* 1.被观察者拥有所有观察者的数组
* 2.事件发布时遍历完整列表,通知每一个观察者
* 老师在把学生全部拉到群里,发送消息,每个学生都接收到
*/
// 观察者
class Observer {
constructor(name) {
this.name = name;
}
receiveMsg(msg) {
console.log(`我是:${this.name},收到了消息${msg}`);
}
}
// 被观察者
class Subject {
constructor() {
this.observers = [];
}
addObserver(ob) {
this.observers.push(ob);
return this;
}
removeObserver(observer) {
let index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(msg) {
this.observers.forEach((ele) => ele.receiveMsg(msg));
}
}
const student1 = new Observer("jack");
const student2 = new Observer("mary");
const student3 = new Observer("alice");
const teacher = new Subject();
// 添加到群里
teacher
.addObserver(student1)
.addObserver(student2)
.addObserver(student3);
// 发送消息
teacher.notify(666);
3.2 发布订阅模式
某某学生说,我请假了,不想接受这么多消息。于是老师说,以后将消息发布在公众号,只有关注的才接受到消息。那么就需要一个发布订阅中心。
代码实现:
/**
* 现在老师不直接拉群,新建一个公众号,公众号提供发布和订阅的方法,如果不订阅就收不到信息
* 1. 订阅者提前订阅对应事件
* 2. 事件发布时所有对应消息订阅回调全部执行
*/
class Event {
// 私有变量 所有消息的消息池
handlers = {};
// 订阅函数
subscribe(msgName, handler) {
if (!this.handlers[msgName]) {
// 如果暂时没有
this.handlers[msgName] = [];
}
// 如果有订阅了
this.handlers[msgName].push(handler);
}
// 取消订阅
unsubscribe(msgName, handler) {
let handlerList = this.handlers[msgName];
if (handlerList) {
let index = handlerList.indexOf(handler);
if (index > -1) {
handlerList.splice(index, 1);
}
}
}
// 发布函数
publish(msgName, ...data) {
if (this.handlers[msgName]) {
this.handlers[msgName].forEach((handler) =>
handler.call(this, ...data)
);
}
}
}
let event = new Event();
event.subscribe("刘老师消息", (msg) => {
console.log(`我是jack,收到了消息${msg}`);
});
event.subscribe("刘老师消息", (msg) => {
console.log(`我是mary,收到了消息${msg}`);
});
event.subscribe("刘老师消息", (msg) => {
console.log(`我是ace,收到了消息${msg}`);
});
setTimeout(() => {
event.publish("刘老师消息", "888", "777");
}, 1000);
3.3 小结
两者对比:
都是为了实现代码之间的解耦和消息通信,两者的区别:
- 观察者模式:一个或多个观察者对象直接监听一个主题对象。当主题对象状态改变时,它会通知所有的观察者对象。观察者和主题之间存在直接的依赖关系。所以,观察者模式通常用于一个对象需要通知其它对象,但又不需要知道这些对象具体是什么的情况。
- 发布-订阅模式:发布者和订阅者通过一个调度中心(也被称为“事件总线”、“消息队列”等)进行通信,发布者发布事件到调度中心,然后调度中心将这些事件派发给相关的订阅者。在发布-订阅模式中,发布者和订阅者是解耦的,它们不知道对方的存在。因此这种模式通常用于大型、复杂系统中,需要处理大量消息的发送和接收。
综上,观察者模式通常用于单一应用/过程内部,当一个对象的改变需要同时改变其它对象,而主对象并不知道具体有多少对象需要改变。而发布-订阅模式更多地应用在异步编程中,或者跨网络的消息传递。
3. 源码解读
通过上面的实现,看两个库的实现就比较简单了。
3.1 mitt
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) {
// 有则删除回调 -1>>>0 = 4294967295 => [4294967295,1]
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);
});
}
}
};
}
3.2 tiny-emitter
function E() {
}
E.prototype = {
/** 订阅 */
on: function (name, callback, ctx) {
// 简写: 判断对象上是否存在e属性没有默认赋值空对象
var e = this.e || (this.e = {});
// 如果订阅的事件名称则push 不存在进行空数组赋值 再进行push保存 保存回调
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
});
// 返回this 为了链式调用
return this;
},
/** 触发 */
emit: function (name) {
// 切割,提取出发布带入的入参,例如emit('msg',1,2,3)=> data为[1,2,3]
var data = [].slice.call(arguments, 1);
// 实际上是在检查this.e这个对象中是否有名为name的属性,如果有就返回其值(应该是数组)的副本,如果没有就返回一个空数组
// 1. this.e || (this.e = {}):这是一个逻辑或操作。如果this.e已经定义,那么就直接使用this.e。否则,将创建一个新的空对象并赋值给this.e。
// 2. [name]:这是在上一步结果(一个对象)中查找名为name的属性。
// 3. (this.e[name] || []):这个是另一个逻辑或操作。如果找到的属性存在,就使用这个属性值,否则就使用一个新的空数组。
// 4. slice():最后,调用数组的slice方法去创建原数组的浅拷贝。
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
// 遍历事件为name的回调
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) {
// 简写: 判断对象上是否存在e属性没有默认赋值空对象
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++) {
// 只有当 evts[i].fn 不等于 callback 且 evts[i].fn._ 也不等于 callback时 才加入liveEvents
// fn._是标记是否只调用一次的
if (evts[i].fn !== callback && evts[i].fn._ !== callback) liveEvents.push(evts[i])
}
}
// 如果有回调就进行赋值 没有就删除掉事件
liveEvents.length ? (e[name] = liveEvents) : delete e[name]
// 链式调用
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);
},
};
3.3 小结
| 库 | 相同点 | 不同点 |
|---|---|---|
| mitt | 1. 轻量级 2. API 设计:都提供了 `on`、`off`、`emit` 3. 运行在浏览器和 Node.js 中 |
1. 支持全局('*')事件监听 2. 获取所有事件 |
| tiny-emitter | 1. 支持设置执行上下文This,支持链式调用 2. 支持多参数多类型传参 |
4. 总结
通过本次源码阅读,学习了:
- 观察者模式和发布订阅的区别
- 了解了mitt和tiny-emiiter的实现原理
一起学习,如果错误,请指正O^O!