前言
Vue2开发过程中,会碰到非父子组件情况,我们大多数会使用Vue提供的自定义实例来解决这个问题,但在Vue3之后就移除了$on
/$off
/$once
/emit
相关API,不再提供自定义实例,而是推荐使用一些第三方库如mitt、tiny-emitter来实现这件事情,接下来就这两个库进行阅读并实现一个自己的事件总线
关于发布订阅
1.发布订阅是一种一对多的对象关系,当一个对象状态改变时候,所有依赖于它的对象都会得到通知
2.订阅者把订阅事件注册到调度中心,发布者去发布事件时,会触发调度中心
对应的订阅者订阅的该事件
mitt
mitt
函数返回一个对象, 将mitt核心源码转JS后,一一拆解理解
function mitt(all) {
all = all || new Map();
return {all,on(type, handler) {//查找 map中是否有这个keyconst 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);});}},
};
}
1.mitt
函数返回一个对象,其中all
属性 为一个Map,用于存储Key Value形式的对象,方便我们存储订阅事件
2.需要注意的是 无符号位移运算符的理解,不然还真没法看懂, 就是一个获取索引的写法。
3.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-mitteron(type, fn, thisArg) {const handlers = all.get(type);const newFn = fn.bind(thisArg); //支持thisnewFn._ = 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));}//默认会触发 * 的事件 - mittconst everys = all.get("*");if (everys) {everys.slice().forEach((every) => every(...evt));}},//支持once事件 - tiny-mitteronce(type, fn, thisArg = window) {let handlers = all.get(type);function onceFn() {//函数执行时 重新获取一次 handlershandlers = 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 {//重置typeall.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包, 总结下相关的知识
1.npm login登录 ,npm publish 进行发布, 使用nrm 管理npm源
2.尝试使用microbundle 打包源文件
3.尝试编写index.d.ts 提供类型声明
使用
npm i harexs-emitter
Github: harexs-emitter
感想
第一次尝试自己编写一个小工具以及发包,有些小兴奋,无符号右位移运算符, 还没读源码开始之前 对于相关的一些知识可以说一概不知。 并且下次可以尝试使用rollup
进行打包, 后续还可以增加单元测试
以及文档
。
最后
为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。
有需要的小伙伴,可以点击下方卡片领取,无偿分享