发布订阅模式(一):tiny-emitter

前言

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

这是源码共读的第8期,链接:【若川视野 x 源码共读】第8期 | mitt、tiny-emitter 发布订阅

铁子们,我是跑不快的猪,好久不见,恭喜北京行程码摘星,感觉马上就能环游地球了。这次继续源码解读的第二篇,选择了 mittytiny-emmitter 包模块的解读,两个包都是为了实现简单的发布订阅者模式,这个模式距离我最近应该就是使用的 vue 了。所以本次也算是为了以后遥遥无期的解读 vue 的源码做一次粗浅的准备吧。本篇是第一篇,解读 tiny-emitter 。之所以如此,好吧,我得承认我刚开始学习 typescript 没多久,而 mitt 模块全文 typescript , 所以为了保证质量,我还要提升下。

发布订阅者模式vs观察者模式

不知道各位是否对这两种模式有熟悉的了解,但是在今天之前我还是懵懂的状态。在网上疯狂查阅后,我更迷茫了。在我查阅的时候,发现了两种说法。

  • 发布订阅者模式是观察者模式的别名,两者没有本质的区别。
  • 发布订阅者模式是观察者模式演变而来 ,两者不是相同的东西。

在查阅了维基百科,与一些同行大佬请教,疯狂机翻了一些外文的文章后。这里说一下我的结论,两者其实不是一回事。
但是两者其实又都解决了类似的问题,就是在一对多的场景中,当“一”发生变化,需要让“多”同时做出相应改变的问题。

就比如现在疫情期间,小区原本通知每天做一次核酸,那小区的居民每天都会去监测点做一次。在疫情得到一定的控制后,小区通知每三天做一次核酸就可以了,那相应的小区居民就会三天做一次。

在上面这个简陋的例子中,宏观来说。 小区 其实就是 观察者模式被观察者 , 同时也是 发布订阅者模式 发布者小区居民 就是观察者,也是 订阅者

不同点

两者的不同点在于:

  • 观察者模式(Observer pattern):观察者和被观察者是在直接进行消息的传递。(紧耦合)
  • 发布订阅模式(Publish-subscribe pattern): 发布者和订阅者通过一个事件处理中心进行信息的传递。(松耦合)
    在这里插入图片描述
    其实稍微延申一下,观察者模式 更像是一种同步的,在一个单一应用中的模式, 观察者被观察者 都有一个比较熟悉的了解。反之,发布订阅 模式更像是一种异步,跨端应用的模式。 发布者订阅者 并不关心到底彼此是谁。仅仅通过事件处理中心能达到自己所需即可。

tiny-emitter

tiny-emitter 就是一个简单实现 发布订阅模式 的npm包,通过它简单的可以达到这一模式的实现。具体的使用方法:tiny-emitter github地址
tiny-emitter 有四个方法提供给使用者。

  • on:订阅事件
  • once:订阅事件且仅被触发一次
  • emit:发布事件
  • off:关闭订阅

整体解析

tiny-emitter 包模块的代码并不复杂,整体来说就是一个函数,在函数的原型对象空间定义了 ononceemitoff 四个函数方法。

// 源码概略
function E() {}
E.prorotype = {
	on: function(name, callback, ctx) {...},
	once: function (name, callback, ctx) {...},
	emit: function (name) {...}
	off: function (name, callback){...}
}
module.exports = E;
module.exports.TinyEmitter = E;

所以当我们引入了这个包模块后,需要将包模块暴露出来的构造函数进行实例化。

// index.js
const E = require('tiny-emitter');
const emitter = new E();

on

E.prototype = {
  on: function (name, callback, ctx) {
    var e = this.e || (this.e = {});		// 这里运用了运算符的短路特性

    (e[name] || (e[name] = [])).push({
      fn: callback,
      ctx: ctx
    });

    return this;
 }

on 方法的源码如上,它支持三个参数 :

  • name:要订阅的事件名称
  • callback:当通过使用 emit 后调用的回调函数
  • ctx:上下文,也就是this的指向。

这个方法代码不多,主要做了下面几个事情:

  • 如果实例化的对象上面没有属性 e,则创建属性 e 作为一个对象,用来保存订阅的事件名称,和触发事件时执行的回调。也就是事件中心。
  • 将事件名作为 e的属性,且是个数组,数组中包含着一个由上下文以及回调函数组成的对象
  • 返回当前E的实例
// e的结构
{
	name: [
		{
			fn: callback,
			ctx: ctx
		},
		{
			fn: callback1,
			ctx: ctx
		}
	]
}

这里有个点要注意下,作者在示例代码中没有说明一个事件可以订阅多次,但是实际上相同的事件可以订阅多次,因为都存入了数组中,并且,相同事件的回调函数以及上下文完全可以不同。

emit

在订阅后,我们就可以用提供的 emit 方法来进行事件的发布了。

E.prototype = {
	emit: function (name) {
	    var data = [].slice.call(arguments, 1);
	    var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
	    var i = 0;
	    var len = evtArr.length;
	
	    for (i; i < len; i++) {
	      evtArr[i].fn.apply(evtArr[i].ctx, data);
	    }
	
	    return this;
	 }
 }

emit 方法的源码如上,它支持一个参数 :

  • name:我们要发布的事件名

这个方法就是发布的主体方法,这个方法做了如下的事情:

  • 通过 callargument 调用数组的 slice 方法,实际上也就是除了事件名字以外的参数,所以,虽然只提供一个参数,但是我们可以传入多个参数,在订阅该事件的回调函数中,也可以用多个参数变量来接。
  • 通过名称找到事件在订阅时的回调函数和上下文集合数组,遍历该数组,通过apply调用每个回调函数。这样就能绑定传入的上下文。
  • 返回当前实例

off

E.prototype = {
	off: function (name, callback) {
	    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++) {
	        if (evts[i].fn !== callback && evts[i].fn._ !== callback)
	          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.length)
	      ? e[name] = liveEvents
	      : delete e[name];
	
	   	 return this;
	  }
}

off 方法的源码如上,它支持两个参数 :

  • name:我们要取消订阅的事件名
  • callback:订阅事件时的回调函数

它做了如下的几件事情

  • 找到存储要取消订阅的事件所对应的数组
  • 通过遍历数组查找出与传入的回调函数不相同的项,然后将这些保存进入一个空数组。
  • 对上述的数组进行判断,如果是空数组,直接删除在 e 中订阅的事件,来达到防止内存泄漏的目的。如源码中的注释所示。
  • 返回当前实例。

这里面有个 evts[i].fn._ !== callback, 是用来和 once 做对应的,看完 once 的解释后可能会更了解,不要在这卡住。

这里有个在使用上比较麻烦的地方,因为作者是用 evts[i].fn !== callback && evts[i].fn._ !== callback 来进行比较,所以我们如果要去取消订阅一个事件的时候,回调函数要和订阅时候的回调函数保持一致。

// 失败的取消订阅
const E = require('tiny-emitter');
const emitter = new E();

emitter.on('event', function() {console.log('hellow world')});
emitter.off('event', function() {console.log('hellow world')});
emitter.emit('event');

// 成功的取消订阅
const E = require('tiny-emitter');
const emitter = new E();
const calback = function(){
	console.log('hellow world');
}
emitter.on('event', calback);
emitter.off('event', calback);
emitter.emit('event');

失败的取消订阅,就是因为函数是引用类型,所以这种写法,虽然看似一样,但是实际上,两个匿名函数在内存中占用的空间是不同的,所以不是相同的方法,导致了取消订阅的失败。

once

E.prototype {
	once: function (name, callback, ctx) {
	    var self = this;
	    function listener () {
	      self.off(name, listener);
	      callback.apply(ctx, arguments);
	    };
	
	    listener._ = callback
	    return this.on(name, listener, ctx);
	}
}

once 方法的源码如上,参数和 on 一样,所以这里不做赘述
它做了这样几件事情:

  • 声明一个函数 listener ,这个函数内部先调用取消订阅的方法,然后再执行回调函数。此时回调函数通过闭包进行访问。
  • listener 声明属性 _ ,保存的值是callback, 这个属性仅仅为了在 off 的时候进行判断。
  • 通过 on 方法将事件与 listener 做一次绑定。
  • 返回当前实例
// e的伪代码
{
	name: [
		{
			fn: listener,   // 函数也是对象,所以可以给函数增加属性这里的listener._就是通过once订阅时候传的回调
			ctx: ctx
		}
	]
}

once 方法中声明的 listener , 其实就是为了让这个订阅只能执行一次。它代替了我们使用 once 订阅时传入的回调函数。如此一来,当我们调用 emit 的时候,就会执行 listener 中的代码,首先调用 off 取消掉订阅,然后执行通过闭包保存住的 callback 函数。
不得不说这种写法,对我来说是比较新颖的,平时完成类似的需求,我可能仅仅会做的就是声明一个计数器。实在惭愧!

我在阅读这部分还是稍微花费了点时间,也因此列在 off 方法后面, 如此一来或许会更好理解?在off 方法中,验证取消订阅的函数用了 evts[i].fn !== callback && evts[i].fn._ !== callback 这样的一个验证判断。在 off 方法解说中,我们也提到了这个 evts[i].fn._ 。其实道理很简单,我们通过 once 订阅后,实际上保存在事件中心的是 listener ,那如果我们订阅后(通过once ) 想立刻取消的话,我们是不知道内部的 listener 函数的,还记得上文提到,函数也是引用类型,我们拿不到自然也就取消不掉。所以,作者这里将原有的 callback 保存在了 listener_ 属性上。如此一来,就可以在 emit 之前进行取消订阅了。

很重要的一点

如果我们打算改变 this 的指向,也就是在 ononce 中传入 ctx 参数,那我们回调函数,不能使用箭头函数。原因这里不做详解,和本文内容相差过大。网上铺天盖地的关于箭头函数this指向的问题,我就不献丑了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值