【源码】mitt、tiny-emitter 之面试官问发布者订阅模式到底是要问什么呢?

1.学习目标

  • 什么是mitttiny-emitter
  • 什么是发布订阅设计模式

2. 资料准备

3.mitt

它足够小,仅有200bytes,其次支持全部事件的监听和批量移除,可以跨框架使用,React 、Vue、jQuery等,,而Vue3.x版本移除了内置的$on$off的方法,改用使用第三方库进行该方法的调用,类似vue2eventBus;

3.1 初识-readme.md

3.3.1 包的使用
$ npm install --save mitt

// using ES6 modules
import mitt from 'mitt'

// using CommonJS modules
var mitt = require('mitt')

<script src="https://unpkg.com/mitt/dist/mitt.umd.js"></script>
window.mitt
3.3.2 用法
import mitt from 'mitt'

const emitter = mitt()

// listen to an event 监听一个事件
emitter.on('foo', e => console.log('foo', e) )

// listen to all events 监听所有事件
emitter.on('*', (type, e) => console.log(type, e) )

// fire an event 触发事件
emitter.emit('foo', { a: 'b' })

// clearing all events 清除所以的事件
emitter.all.clear()

// working with handler references: 使用处理程序引用
function onFoo() {}
emitter.on('foo', onFoo)   // listen
emitter.off('foo', onFoo)  // unlisten

3.2 mitt 源码整体结构

下载下来发现源码是ts文件,由于使用ts不是很熟,此次将ts转换成js,进行阅读的:

  1. npm版本得在8.x.x
  2. 没在这个版本的可以自定义升级,也可以升级到指定版本npm install -g npm@8.1.4
  3. 下载ts npm i -g typescript
  4. 检查版本 tsc -v
  5. 找到你要转的ts文件,比如你的文件名在src下,叫index.ts
  6. 进行文件转化执行命令: tsc src/index.ts,即可
function mitt(all) {
    all = all || new Map();
    return {
        all: all,
        on: function (type, handler) {
            // code ...
        },
        off: function (type, handler) {
            // code ...
        },
        emit: function (type, evt) {
            // code ...
        }
    };
}

来看看源码:


exports.__esModule = true;

/**
 * Mitt: Tiny (~200b) functional event emitter / pubsub.
 * 导出一个mitt([all]),返回一个emitter对象,包含all、on(type,handler)、off(type, handler)和emit(type, [evt])这几个属性
 */
function mitt(all) {
	// 传入all参数用来存储时间类型和时间处理函数的映射Map,如果不传,就new Map()赋值给all
	// Map 对象保存键/值 对,是键/值对的集合, 用于管理订阅者
	all = all || new Map();
	return {

		/**
         * A Map of event names to registered handler functions.
         * all 是发布订阅模式里的调度中心
         */
		all,

		/**
         * Register an event handler for the given type.为给定类型注册事件处理程序。
         * 定义函数on 来注册事件,以type为属性,[hanlder]为属性值,存储在all中
         * [hanlder]为属性值,属性值为数组的原因是可能存在监听一个事件,多个处理程序
         * 订阅者订阅事件的函数
         */
		on (type, handler) {
			//获取指定type 对应的handler
			let handlers = all.get(type);
			//如果存在,为已有的handler添加handler,在热更新的时候 该方法会执行多次的原因
			if (handlers) {
				handlers.push(handler);
			}
			else {
				// 如果不存在,为指定type添加数组,元素为handler
				all.set(type, [handler]);
			}
		},

		/**
         * Remove an event handler for the given type.删除给定类型的事件处理程序
         * off 取消某个事件的某个处理函数(订阅者删除订阅事件的函数)
         */
		off (type, handler) {
			// 获取指定type 对应的handler
			let handlers = all.get(type);
			// 如果type存在,且传入handler
			if (handlers) {
				if (handler) {

					/**
                     * 会在type中寻找与传入的handler相对应的handler
                     * 如果存在,就会返回在数组中的位置,进行删除
                     * 如果不存在,就会直接返回原数组
                     */
					handlers.splice(handlers.indexOf(handler) >>> 0, 1);
				}
				else {
					//如果没有传入handler 默认当前type下全部清空
					all.set(type, []);
				}
			}
		},
		/**
         * Invoke all handlers for the given type.调用给定类型的所有处理程序
         * 发布者发布事件的函数
         */
		emit (type, evt) {
			// 获取指定type 对应的handler
			let handlers = all.get(type);
			// 如果存在
			if (handlers) {
				//浅拷贝hndlers,并循环对所有handler执行参数为evt的方法( 把handeles中的每一函数都触发一次)
				//slice() slice()中的参数如果都不传,就代表从开始到结尾,最后返回一个含有被提取元素的新数组
				// evt:如果是对象最好,传入多个参数只会取到第一个
				handlers
					.slice()
					.map((handler) => {
						handler(evt);
					});
			}
			//* 代表所有的事件, 判断是否为*的函数队列
			handlers = all.get('*');
			if (handlers) {
				//如果存在的话只要是*的队列函数,都会执行一次。
				handlers
					.slice()
					.map((handler) => {
						handler(type, evt);
					});
			}
		}
	};
}
//默认导出
exports.default = mitt;

/**
 * 1.>>> 0 的作用
 * 
 */
  • >>> 0表达式
  1. >>>运算符执行五符号右移位运算。
  2. 它把无符号的 32 位整数所有数位整体右移。
  3. 对于无符号数或正数右移运算,无符号右移与有符号右移运算的结果是相同的
  4. 对于负数来说,无符号右移将使用 0 来填充所有的空位
  5. 同时会把负数作为正数来处理,所得结果会非常大所以,使用无符号右移运算符时要特别小心,避免意外错误
  6. '-1' >>> 0 => 4294967295
  7. handlers.indexOf(handler) >>> 0
    判断数组里面是否存在这个,不存在把-1 =>0
    存在必然能删,不存在也删不着,很优雅
+ handlers = all.get('*')
//这个是配合emitter.on('*', (type, e) => console.log(type, e) )这个使用的,意思是会监听所有通过emitter.on()注册过的事件,例:

emitter.on('foo', e => console.log('foo', e) ) // listen to all events 

emitter.on('*', (type, e) => console.log(type, e) )

emitter.emit('foo', { a: 'b' })
//实际会打印两遍

4. tiny-emitter

tiny-emitter是一个轻型的是事件发射器工具库主要用来实现一个简易的基于监听发布者模式的事件派发和接收器
vue3.x版本中不能在使用eventBus了,不过官方有替代品 mitttiny-emitter

4.1 初识–readme.md

4.1.1 安装
npm install tiny-emitter --save
4.1.2 使用
var Emitter = require('tiny-emitter');

//或者,您可以跳过初始化步骤,转而要求使用微型发射器/实例。这将引入一个已经初始化的发射器。
var emitter = require('tiny-emitter/instance');

var emitter = new Emitter();

emitter.on('some-event', function (arg1, arg2, arg3) {
 //
});

emitter.emit('some-event', 'arg1 value', 'arg2 value', 'arg3 value');
4.1.3实例方法
on(event, callback[, context])订阅事件
  event - 要订阅的事件名称
  callback - emit触发事件时要调用的函数
  context - (可选的) - 将事件回调绑定到的this上下文

once(event, callback[, context])只订阅一次事件
  event - 要订阅的事件的名称
  callback - emit发出事件时要调用的函数
  context - (可选的) - 将事件回调绑定到的this上下文

off(event[, callback])取消订阅事件或所有事件。如果没有提供回调,它将取消对所有事件的订阅。
  event - 要取消订阅事件的名称
  callback - 绑定到事件时使用的回调函数
  
emit(event[, arguments...])触发命名事件
  event - emit触发事件时的事件名称
  arguments... - 传递给事件的回调函数任何数量的参数
4.1.3 项目中使用
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import Emitter from 'tiny-emitter'

const emitter = new Emitter()
const app = createApp(App)
// 绑定为全局可用
app.config.globalProperties.emitter = emitter

app.mount('#app')
4.1.4 组件中使用
// 接收事件方法
this.emitter.on('changeLoading', (news) => {
    this.isShowLoding = news
})
// 发送事件方法
this.emitter.emit('changeLoading', true)

4.2 tiny-emitter源码

//定义一个空构造函数E
function E () {}

//E.prototype = {} 是为了继承
// clipboard就是继承的这个
//[链接](https://github.com/zenorocha/clipboard.js/blob/master/src/clipboard.js#L26)

E.prototype = {

  // on 订阅事件并绑定回调函数
  on: function (name, callback, ctx) {
    //存在e使用e,不存在使用空对象
    var e = this.e || (this.e = {});
    //e[name]存在直接push/不存在定义为空数组[] 在push事件name 对应的处理函数callback
    (e[name] || (e[name] = [])).push({
      fn: callback,
      ctx: ctx
    });
    //保持上下文引用可以实现链式调用
    return this;
  },

  // 只订阅一次事件,执行完会被off掉,不会再执行
  once: function (name, callback, ctx) {
    var self = this;
    //定义一个函数listener,
    function listener () {
      //把变量e里name 对应的监听事件listener去掉
      self.off(name, listener);
      // 执行一次这个监听函数
      callback.apply(ctx, arguments);
    };

    listener._ = callback
    return this.on(name, listener, ctx);
  },

  //发布事件,并执行订阅事件对应的回调函数
  emit: function (name) {
    // 获取参数,此处可以传递多个参数
    var data = [].slice.call(arguments, 1);
    //获取name 事件对应的所有回调函数
    var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
    var i = 0;
    var len = evtArr.length;

    for (i; i < len; i++) {
      //遍历执行回调函数并接收参数data
      evtArr[i].fn.apply(evtArr[i].ctx, data);
    }

    return this;
  },

  // 取消订阅事件或者所有事件。 如果没有提供回调,他将取消所有事件的订阅
  off: function (name, callback) {
    //存在e 使用 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++) {
        //如果某个回调函数与指定的回调函数不一致,存入liveEvents
        if (evts[i].fn !== callback && evts[i].fn._ !== callback)
          liveEvents.push(evts[i]);
      }
    }
   
    // 如果有指定的回调函数,修改事件name的回调函数数组为liveEvents,否则删除事件name
    (liveEvents.length) ? e[name] = liveEvents : delete e[name];

    return this;
  }
};

//导出构造函数E
module.exports = E;
module.exports.TinyEmitter = E;

总结

总结之前先来看看什么是发布订阅模式?

在现实开发中,比如3个文件之间要完成一个互相通信的功能,比如a,b,c,a和b 的方法都需要在c中调用,此时就会非常的麻烦,也会发生一些不可避免的情况,此时发布订阅模式就可以解决这个问题。

  • 发布-订阅模式里面包含了三个模块: 发布者,订阅者和调度中心
  • 这里处理中心相当于微信公众号
  • 发布者相当与某个公众号负责人
  • 订阅者相当于用户
  • 当用户关注了这个公众号,每当作者发布了一篇文章,公众号就会通知用户作者发新文章啦,这样但凡作者通过公众号发文章,订阅者就可以及时收到,这样在结合下面的图应该很好理解了。
    在这里插入图片描述
    mitttiny-emitter就是实现了发布—订阅者模式的库,也可以说是工具。

感受

  • 源码思想不难理解,再通过调试也可以更好的理解,比较难以理解的往往就是不太熟悉的写法
  • 跟大佬们的区别可不是一块砖,还是慢慢一块一块搬吧,正所谓:不积跬步,无以至千里;不积小流,无以成江海
  • 值得一说,一期期下来,那见识可以比一期都不读的有见识多喽!
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值