实现一个全局事件总线并发布到NPM
本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第8期 | mitt、tiny-emitter 发布订阅
本文将梳理发布订阅设计模式下事件总线的实现,并了解tiny-emitter、mitt 两个库的实现
前言
Vue2开发过程中,会碰到非父子组件情况,我们大多数会使用Vue提供的自定义实例来解决这个问题,但在Vue3之后就移除了$on
/$off
/$once
/emit
相关API,不再提供自定义实例,而是推荐使用一些第三方库如mitt、tiny-emitter来实现这件事情,接下来就这两个库进行阅读并实现一个自己的事件总线
关于发布订阅
- 发布订阅是一种一对多的对象关系,当一个对象状态改变时候,所有依赖于它的对象都会得到通知
- 订阅者把订阅事件注册到调度中心,发布者去发布事件时,会触发
调度中心
对应的订阅者订阅的该事件
mitt
mitt
函数返回一个对象, 将mitt核心源码转JS后,一一拆解理解
function mitt(all) {
all = all || new Map();
return {
all,
on(type, handler) {
//查找 map中是否有这个key
const handlers = all.get(type);
//存在的话 就给这个key 增加事件
if (handlers) {
handlers.push(handler);
} else {
//不存在就创建 value是一个数组
all.set(type, [handler]);
}
},
off(type, handler) {
//取消订阅
const handlers = all.get(type);
if (handlers) {
// handlers 对应的就是事件的数组
if (handler) {
//无符号位移运算符
//把 32 位数字中的所有有效位整体右移,再使用符号位的值填充空位。移动过程中超出的值将被丢弃
//对于负数来说,无符号右移将使用 0 来填充所有的空位,同时会把负数作为正数来处理,所得结果会非常大所以
//如果找不到的话 -1 >>> 0 返回的结果是4294967295 就相当于无效了 , 这个操作符 省去了 判断-1的操作。。
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
} else {
//如果没传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);
});
}
},
};
}
mitt
函数返回一个对象,其中all
属性 为一个Map,用于存储Key Value形式的对象,方便我们存储订阅事件- 需要注意的是 无符号位移运算符的理解,不然还真没法看懂, 就是一个获取索引的写法。
on
订阅,emit
发布,off
移除订阅
使用
const emitter = mitt();
function onFoo() {
console.log('Harexs')
}
emitter.on("foo", onFoo);
emitter.off("foo", onFoo);
console.log(emitter);
tiny-emitter
tiny-emitter
的实现与mitt
返回对象不同,它通过原型挂载的形式,所有创建的对象共享原型上的方法使用
let mitt = new E();
console.dir(Object.getPrototypeOf(mitt));
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) {
//创建变量e 指向this的属性 不存在则创建
var e = this.e || (this.e = {});
//返回对应name的事件列表不存在则为空数组
//push一个对象,包含事件回调以及this指向
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx,
});
//最后返回this对象
return this;
},
once: function (name, callback, ctx) {
var self = this;
//实现once 的大前提是我们需要传入命名函数,存在引用关系可以让我们移除
function listener() {
//函数被执行时移除 自身
self.off(name, listener);
// 并调用一次 once传入的callback
callback.apply(ctx, arguments);
}
//用于off移除时进行对比
listener._ = callback;
//return on会返回this 并绑定name对应的linstener事件
return this.on(name, listener, ctx);
},
emit: function (name) {
//[]是字面量形式,这里实际是 Array.slice.call(arguments,1)
//data 截取 name参数之后 剩下的参数 返回一个新数组
//这里是取 name以外的剩余参数
var data = [].slice.call(arguments, 1);
//取this下的e对象下 name属性的值 不存在的情况 返回一个空数组
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
var i = 0;
var len = evtArr.length;
//如果对应name的 事件数组是空的那么这个for 也不会执行
for (i; i < len; i++) {
//依次将每个值(对象) 的fn属性的事件 通过apply调用 并传入 this指向 以及data剩余参数
evtArr[i].fn.apply(evtArr[i].ctx, data);
}
//return this
return this;
},
off: function (name, callback) {
//同上 取e 不存在就创建
var e = this.e || (this.e = {});
//找到对应的订阅者
var evts = e[name];
var liveEvents = [];
//当订阅 以及 要取消的callback都存在时
if (evts && callback) {
for (var i = 0, len = evts.length; i < len; i++) {
//遍历数组对象 fn与callback不相等 并且 fn的_属性不等于callback
//这个_ 属性可以在once中找到 ,它就是为了对比是否相等
//想要全等的前提是它们传入的是 命名函数
//匿名函数本质来说也是一个对象,每个创建的对象之间判断必定是false的
//从堆上来说 它们的堆地址是不一样,而命名函数 由于具备栈的指向,所以才具有相等的条件
if (evts[i].fn !== callback && evts[i].fn._ !== callback)
//如果不相等则将数组中的这个对象 push进liveEvents
//其实就是不符合移除条件的对象 push进一个新数组 到时候重新赋值给回e[name]
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 存在内容则 给 e[name]重新赋值, 否则直接移除 这个name属性
liveEvents.length ? (e[name] = liveEvents) : delete e[name];
return this;
},
};
它的使用 和mitt大同小异,但多出一个once
的API, 并且支持我们 绑定this指向
var e1 = new E();
e1.once(
"lff",
() => {
console.log("lff", this);
});
e1.emit("lff");
实现
结合实现
结合mitt
和 tiny-emitter
的特点,接下来动手实现一个自己的版本
const Harexs_Mitt = (all = new Map()) => {
return {
all,
//支持this指向 - tiny-mitter
on(type, fn, thisArg) {
const handlers = all.get(type);
const newFn = fn.bind(thisArg); //支持this
newFn._ = fn; //用于删除时函数对比
if (handlers) {
//通过bind 支持this指向
handlers.push(newFn);
} else {
all.set(type, [newFn]);
}
},
emit(type, ...evt) {
const handlers = all.get(type);
if (handlers) {
//由于可能会碰到once splice移除的操作 导致索引变化问题 不能正确触发函数
//所以使用 slice 创建一个副本来 执行
handlers.slice().forEach((handler) => handler(...evt));
}
//默认会触发 * 的事件 - mitt
const everys = all.get("*");
if (everys) {
everys.slice().forEach((every) => every(...evt));
}
},
//支持once事件 - tiny-mitter
once(type, fn, thisArg = window) {
let handlers = all.get(type);
function onceFn() {
//函数执行时 重新获取一次 handlers
handlers = all.get(type);
fn.apply(thisArg, arguments);
//一旦执行后 就移除本次订阅的事件
handlers.splice(handlers.indexOf(onceFn) >>> 0, 1);
}
//用于移除时对比
onceFn._ = fn;
if (handlers) {
handlers.push(onceFn);
} else {
all.set(type, [onceFn]);
}
},
off(type, fn) {
let handlers = all.get(type);
let newFnAry = [];
if (handlers) {
if (fn) {
handlers.forEach((handler) => {
if (handler._ !== fn) {
newFnAry.push(handler);
}
});
//如果有匹配到的函数 则使用接收了这些函数的newFnAry赋值
newFnAry.length
? (handlers = newFnAry.slice())
: all.set(type, []);
} else {
//重置type
all.set(type, []);
}
}
},
};
};
const emit = Harexs_Mitt();
function removeFn() {
console.log(arguments);
}
emit.once(
"lff",
function (e) {
console.log(this, e);
},
{ name: "harexs" }
);
emit.on("lff", (e) => console.log(e));
emit.off("lff", (e) => console.log(e));
emit.on("lff", removeFn);
emit.off("lff", removeFn);
console.log(emit);
Vue中使用
// utils.js
import emitter from 'harexs-emitter'
export const emitFire = emitter()
//brother1.vue
import {emitFire} from 'your file url/utils.js'
emitFire.on('harexs',()=>console.log('harexs'))
//brother2.vue
import {emitFire} from 'your file url/utils.js'
emitFire.emit('harexs')
发布
算是我第一个尝试发布的npm包, 总结下相关的知识
- npm login登录 ,npm publish 进行发布, 使用nrm 管理npm源
- 尝试使用microbundle 打包源文件
- 尝试编写index.d.ts 提供类型声明
使用
npm i harexs-emitter
Github: harexs-emitter
感想
第一次尝试自己编写一个小工具以及发包,有些小兴奋,无符号右位移运算符, 还没读源码开始之前 对于相关的一些知识可以说一概不知。 并且下次可以尝试使用rollup
进行打包, 后续还可以增加单元测试
以及文档
。
转载自:https://juejin.cn/post/7144930706072797191