实现一个全局事件总线并发布到NPM

548 阅读5分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,     点击了解详情一起参与。

这是源码共读的第8期 | mitt、tiny-emitter 发布订阅

本文将梳理发布订阅设计模式下事件总线的实现,并了解tiny-emitter、mitt 两个库的实现

前言

Vue2开发过程中,会碰到非父子组件情况,我们大多数会使用Vue提供的自定义实例来解决这个问题,但在Vue3之后就移除了$on/$off/$once/emit 相关API,不再提供自定义实例,而是推荐使用一些第三方库如mitt、tiny-emitter来实现这件事情,接下来就这两个库进行阅读并实现一个自己的事件总线

关于发布订阅

image.png

  1. 发布订阅是一种一对多的对象关系,当一个对象状态改变时候,所有依赖于它的对象都会得到通知
  2. 订阅者把订阅事件注册到调度中心,发布者去发布事件时,会触发调度中心对应的订阅者订阅的该事件

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);
                    });
            }
    },
};
}
  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));

image.png

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");

实现

结合实现

结合mitttiny-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包, 总结下相关的知识

  1. npm login登录 ,npm publish 进行发布, 使用nrm 管理npm源
  2. 尝试使用microbundle 打包源文件
  3. 尝试编写index.d.ts 提供类型声明

使用

npm i harexs-emitter

Github: harexs-emitter

感想

第一次尝试自己编写一个小工具以及发包,有些小兴奋,无符号右位移运算符, 还没读源码开始之前 对于相关的一些知识可以说一概不知。 并且下次可以尝试使用rollup进行打包, 后续还可以增加单元测试以及文档