1.阅读前准备

这两个库都是在vue中非常熟悉、使用频率很高的事件传递方法之一,而Vue3.x版本移除了内置的$on、$off的方法,改用使用第三方库进行该方法的调用

阅读前先温习一下正常的使用方法:

1.1 使用方法

首先 $on和$emit的事件必须在一个公共实例上才会被触发
在vue项目开发时,经常会遇到兄弟、爷孙组件的传值情况,此时通常会使用Vue实例作为中央事件处理总线,将事件放置在全局中触发

  1. vue.prototype.$bus = new Vue();
  2. this.$bus.$emit('test','gelx')
  3. // mounted中接受函数
  4. mounted() {
  5. this.$bus.$on('test',info => {
  6. console.log(info)
  7. })
  8. }

1.2 简单了解Map

源码中大量使用的Map的方法,由于之前没有接触过Map,因此做一个小小的预习

1.2.1 什么是map

Map对象保存键/值 对,是键/值对的集合

1.2.2与Object对象的不同点
  • Map的键可以是任意值
  • Map的键值是有顺序的,遍历时会按插入的顺序返回键值
  • 通过size属性可以获取Map的键值对个数
  • Map可以进行迭代

1.2.3常用的属性、方法
  • 属性
    • size 获取键值对数量
  • 方法
    • clear() 清空map对象
    • delete(key) 移除某个指定元素
    • get(key) 返回某个指定元素
    • has(key) 检测是否存在某个元素
    • set(key,value) 添加至Map对象的key 键和对应的value 值

2.开始阅读

今天主要有两部分源码
第一部分mitt: https://github.com/developit/mitt
第二部分tiny-emitter: https://github.com/scottcorgan/tiny-emitter

学习的目标就是了解和学习发布订阅者模式

3.源码正文

3.1 miit源码阅读

首先开始阅读mitt源码
具体源码目录如下,主要有两部分:源码主体、test测试用例
image.png
第一件事,tsc src/index.ts
希望自己可以早点学会ts,进行ts源码阅读……

3.1.1 源码主体

整个源码函数主要实现了四个功能:
1:接收一个外来参数,或直接新建一个Map对象
2:实现on函数
3:实现off函数
4:实现emit函数

  1. function mitt(all) {
  2. all = all || new Map();
  3. return {
  4. all: all,
  5. on: function (type, handler) {
  6. // code ...
  7. },
  8. off: function (type, handler) {
  9. // code ...
  10. },
  11. emit: function (type, evt) {
  12. // code ...
  13. }
  14. };
  15. }

3.1.2 on(),它是一个订阅者订阅事件的函数
  1. on: function (type, handler) {
  2. // 获取指定type对应的handler
  3. var handlers = all.get(type);
  4. // 如果存在,就为已有的处理上push一个新的handler
  5. if (handlers) {
  6. handlers.push(handler);
  7. }
  8. // 如果不存在,就新增一个type,且对应的处理由数组存放,为了方便添加多个处理
  9. else {
  10. all.set(type, [handler]);
  11. }
  12. },

3.1.3 off(),它是一个订阅者删除订阅事件的函数
  1. off: function (type, handler) {
  2. // 同样获取指定type的handler
  3. var handlers = all.get(type);
  4. // 如果传入了handler
  5. if (handlers) {
  6. // 且这个type存在时
  7. if (handler) {
  8. // 会在type中寻找与传入的handler相对应的handler
  9. // 如果存在,就会直接返回在数组中的位置,进行删除
  10. // 如果不存在,就会直接返回原数组,这里下面会继续分析
  11. handlers.splice(handlers.indexOf(handler) >>> 0, 1);
  12. }
  13. // 如果没有传入handler
  14. // 默认当前type下全部清空
  15. else {
  16. all.set(type, []);
  17. }
  18. }
  19. },

这部分出现了一个 >>> 0 的表达式,进一步了解这个表达式的意义

js中>>表示有符号位移,>>>表示无符号位移

  • 有符号位移表示 右边移去一个0,左边补充一个1
  • 无符号位移表示 右边移去一个0,左边补充一个0

当需要操作正数的时候,是不会有差别的
但操作负数的时候,就会有非常大的差别

负数在二进制中的表达是以补码的形式存在,也就是正常的二进制码的反码再加+

负数进行无符号的位移时,会在左边补充一个0,变成了正数,也就是目前数组的长度,删除一个最大长度上的第一位(空值),所以这部分会将内容全部返回

3.1.4 emit(),它是一个发布者发布事件的函数
  1. emit: function (type, evt) {
  2. // 获取指定type对应的handlers
  3. var handlers = all.get(type);
  4. // 如果存在
  5. if (handlers) {
  6. // 浅拷贝handlers,并循环对所有handler执行参数为evt的方法
  7. handlers
  8. .slice()
  9. .map(function (handler) {
  10. handler(evt);
  11. });
  12. }
  13. // 如果不存在
  14. // 找全局上绑定的handlers
  15. handlers = all.get('*');
  16. // 如果全局上纯在handlers
  17. // 依次进行执行,并传递当前触发的type
  18. if (handlers) {
  19. handlers
  20. .slice()
  21. .map(function (handler) {
  22. handler(type, evt);
  23. });
  24. }
  25. }

这部分的emit仅可传递一个参数,如果需要传递多个参数,需要以对象或数组的形式传入

3.2.1 tiny-emitter 部分

这个工具库实现的功能与mitt大同小异,同样实现了$on $once $off $emit的功能,但具体的使用上会有一些小的差异

3.2.2 使用方式

先看看官方文档给出的使用案例:

  1. var Emitter = require('tiny-emitter');
  2. var emitter = new Emitter();
  3. emitter.on('some-event', function (arg1, arg2, arg3) {
  4. //
  5. });
  6. emitter.emit('some-event', 'arg1 value', 'arg2 value', 'arg3 value');

从这里就可以看到与miit一个比较大的不同是:emit可以传递多个参数

3.2.3 源码阅读

首先依旧是tsc index.d.ts

这个库是定义了一个空的函数,通过在它的原型上定义了4种方法:

E.prototype = { on: function (name, callback, ctx) { //code… },

once: function (name, callback, ctx) { //code… },

emit: function (name) { // code… },

off: function (name, callback) { // code… } };

  1. <a name="AgpFI"></a>
  2. ##### 3.2.4 on() 订阅事件
  3. ```javascript
  4. on: function (name, callback, ctx) {
  5. // 判断是否存在e,如果不存在,就是用空对象
  6. // mitt是用的是Map
  7. var e = this.e || (this.e = {});
  8. // 并判断传入的name是否存在,用数组的方式存储
  9. (e[name] || (e[name] = [])).push({
  10. fn: callback,
  11. ctx: ctx
  12. });
  13. // 返回this,保持上下文依旧可以调用
  14. return this;
  15. },

once() 只执行一次订阅时间
  1. once: function (name, callback, ctx) {
  2. var self = this;
  3. function listener () {
  4. // 执行完成后调用off注销事件
  5. self.off(name, listener);
  6. callback.apply(ctx, arguments);
  7. };
  8. listener._ = callback
  9. return this.on(name, listener, ctx);
  10. },

emit() 发布事件
  1. emit: function (name) {
  2. // 创建一个data数组,并获取参数,这里可以拿到多个参数
  3. var data = [].slice.call(arguments, 1);
  4. var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
  5. var i = 0;
  6. var len = evtArr.length;
  7. for (i; i < len; i++) {
  8. // 遍历执行回调函数,并接收传入的data参数
  9. evtArr[i].fn.apply(evtArr[i].ctx, data);
  10. }
  11. return this;
  12. },

off() 取消事件绑定
  1. off: function (name, callback) {
  2. var e = this.e || (this.e = {});
  3. // 获取e上name中对应的所有的回调函数
  4. var evts = e[name];
  5. var liveEvents = [];
  6. // 如果off传入了回调函数
  7. if (evts && callback) {
  8. // 会遍历name中所有的函数
  9. for (var i = 0, len = evts.length; i < len; i++) {
  10. // 如果存在与传入的回调函数不同的函数,就push进数组中
  11. if (evts[i].fn !== callback && evts[i].fn._ !== callback)
  12. liveEvents.push(evts[i]);
  13. }
  14. }
  15. // 最后如果这个数组存在内容,就保留继续执行,如果不存在,就整个删掉
  16. (liveEvents.length)
  17. ? e[name] = liveEvents
  18. : delete e[name];
  19. return this;
  20. }

4.总结

又是一篇拖了很久的文章,除了工作的原因,还有对基础的js不是很熟的原因,不过通过阅读这两个开源库也学到了:

  • Map的具体用法,与Object的异同点
  • 发布订阅的设计模式
  • 事件传递的原理,也能在实际工作中使用时,更清楚为什么要这么使用