前言

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

这是源码共读的第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._ = callbackreturn 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指向的问题,我就不献丑了。

发布订阅模式(一):tiny-emitter相关推荐

  1. 从东京奥运会看js设计模式之发布订阅模式

    开篇废话:本篇文章介绍发布-订阅模式,想必很多人听说过有一种观察者模式,网上既有资料说这是两种不同的设计模式,也有说这是一种模式,我倾向于认同他们是同一种设计模式.不必过于纠结 开篇楔子:东京奥运会已 ...

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

    背景 最近在研究react的状态管理器zustand时,研究源码时发现其组件注册绑定是通过观察者模式结合react hooks实现更新的.而联想之前写vue的时候,经常会用到vue内置的自定义事件进行 ...

  3. Redis 笔记(10)— 发布订阅模式(发布订阅单个信道、订阅信道后的返回值分类、发布订阅多个信道)

    1. 发布-订阅概念 发布-订阅 模式包含两种角色,分别为发布者和订阅者. 订阅者可以订阅一个或者若干个频道(channel): 而发布者可以向指定的频道发送消息,所有订阅此频道的订阅者都可以收到此消 ...

  4. Redis 高级特性(2)—— 发布 订阅模式

    Redis 高级特性 -- 发布订阅 1. 发布-订阅介绍 "发布-订阅"模式包含两种角色,分别为发布者和订阅者.订阅者可以订阅一个或者若干个频道(channel),而发布者可以向 ...

  5. 【EventBus】发布-订阅模式 ( Android 中使用 发布-订阅模式 进行通信 )

    文章目录 一.拷贝 发布-订阅模式 相关类 二.完整代码示例 一.拷贝 发布-订阅模式 相关类 将上一篇博客 [EventBus]发布-订阅模式 ( 使用代码实现发布-订阅模式 ) 写的 发布-订阅模 ...

  6. 【EventBus】发布-订阅模式 ( 使用代码实现发布-订阅模式 )

    文章目录 一.发布-订阅模式 二.代码实现发布-订阅模式 1.订阅者接口 2.订阅者实现类 3.发布者 4.调度中心 5.客户端 一.发布-订阅模式 发布订阅模式 : 发布者 Publisher : ...

  7. 【EventBus】发布-订阅模式 ( EventBus 组成模块 | 观察者模式 )

    文章目录 一.发布-订阅模式 二.EventBus 组成模块 三.观察者模式 一.发布-订阅模式 发布订阅模式 : 发布者 Publisher : 状态改变时 , 向 消息中心 发送事件 ; 订阅者 ...

  8. JavaScript 设计模式之观察者模式与发布订阅模式

    前言 在软体工程中,设计模式(design pattern)是对软体设计中普遍存在(反复出现)的各种问题,所提出的解决方案. 设计模式并不直接用来完成程式码的编写,而是描述在各种不同情况下,要怎么解决 ...

  9. 点击事件调用匿名函数如何传参_事件发布/订阅模式的简单实现

    这是一种广泛应用于异步编程的模式,是回调函数的事件化,常常用来解耦业务逻辑.事件的发布者无需关注订阅的侦听器如何实现业务逻辑,甚至不用关注有多少个侦听器存在.数据通过消息的方式可以灵活的传递. --& ...

  10. 利用zookeeper实现发布订阅模式

    zookeeper应用 发布订阅 zk实现的方式是推拉结合,Client想服务端注册自己需要关注的节点,一旦节点的数据发生变更,那么Server会向对应的客户端发送Watcher事件通知,客户端接收到 ...

最新文章

  1. Tungsten Fabric SDN — 流量调试手段
  2. LeetCode Paint House II
  3. STM32开发 -- 继电器测试
  4. containsObject 总是不含有,你会用吗
  5. Sql数据库批量清理日志
  6. 火遍全网的Hutool,如何使用Builder模式构建线程池
  7. Mysql梳理(单表查询)
  8. Unity教程之-Unity Attribute的使用总结
  9. biee12c连接hive_BIEE 12c Linux下连接Hadoop Hive
  10. BUUCTF------相册
  11. Cisco路由器配置命令
  12. CNN卷积神经网络原理详解(上)
  13. spring 的bean 作用域
  14. 钉钉isv接入三方应用授权鉴权流程
  15. 一个简单移动页面ionic打包成app
  16. 通过任意数量点拟合曲线
  17. iOS 客户端 IM 以及列表 UI 框架
  18. css图片压缩不变形
  19. 一台微型计算机的好坏 主要取决于,计算机一级MSOffice应用选择题
  20. 命令行使用oracle19c_把oracle19c数据导入oracle11g

热门文章

  1. 抖音快手YY西瓜斗鱼花椒虎牙等直播平台实时录制
  2. 太阳直射点纬度计算公式_高中地理——每日讲1题(极昼、极夜、太阳高度角、太阳辐射)...
  3. 【办公基本软件】万彩办公大师教程丨PDF压缩工具
  4. 珠宝行业电子秤串口程序开发
  5. linux vsftpd共享位置,文件共享服务之vsftpd
  6. linux 执行 ktr脚本,kettle在linux下面用于shell脚本执行:转换或者作业
  7. Pintos-斯坦福大学操作系统Project详解-Project1
  8. flac格式歌曲如何转换成mp3格式,flac转mp3详细图文教程
  9. nfc门禁卡的复制和迁移
  10. apache评分表的意义_APACHE-II评分系统表.doc