头图

短小精悍的发布订阅库 mitt

介绍

mitt 是一个小而美的发布-订阅库,短短的几十行代码,小于 200b 的体积,提供三个重要的 API。然而麻雀虽小,五脏俱全。

发布-订阅模式

发布-订阅模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象。这个主题对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。

用法

有这样的一个需求,小明想买 100w 以内的二手房,小华想买 150w 左右的二手房,但是房产中介告诉他俩,暂时没有他们想要的房源,中介的工作人员留下他俩的联系方式,一旦有合适的房源就通过电话通知。这就是一个经典使用发布-订阅的模式的场景。请看下面的代码。

const mitt = require("mitt");

const houseAgents = mitt();

houseAgents.on("xiaoming", () => {
  console.log("有 100w 以内的房源了");
});

houseAgents.on("xiaohua", () => {
  console.log("有 150w 左右的房源了");
});

过了一段时间,中介收到 150w 左右的房源,立马通知 xiaohua。

houseAgents.emit("xiaohua");

源码分析

mitt 通过几十行代码实现了发布-订阅机制。我们来剖析一下 mitt 源码

function mitt(all) {
  all = all || Object.create(null);

  return {
    on: function () {
      /* some code*/
    },
    emit: function () {
      /* some code*/
    },
    off: function () {
      /* some code*/
    },
  };
}

mitt 库的源码中只有一个 mitt 函数。它接收 all 参数,返回一个对象,该对象包含 on、emit、off 三个方法。

all = all || Object.create(null);

mitt 方法接收 all 参数,all 用来存储监听的事件。当传入的 all 的值是 undefinednull""falseNaN 等值时,all 的值默认为 Object.create(null), 它是一个没有 __proto__ 属性的对象。

实际上,这里缺少类型处理。比如 mitt(true), 就会导致错误。

const mitt = require("mitt");
const ob = mitt(true);

ob.on("a", () => {});
ob.emit("a", "dd");

建议 all 的类型是对象类型或者不传。推荐传入空对象或数组。

const mitt = require("mitt");
const ob = mitt();
/* 
  or
  const ob = mitt([]);
  const ob = mitt({});

*/

接下来,我们来看看 mitt 里非常重要的三个方法。

// ...
on: function on(type, handler) {
  (all[type] || (all[type] = [])).push(handler);
}

// ...

on 接收两个参数,type 类型和 handler 事件处理方法。方法体内仅有一行非常优雅的代码。当该类型存在时,就将其事件处理追加到数组后面,当该类型不存在时,初始化一个空数组,用来存储该类型的事件处理方法。

emit: function emit(type, evt) {
  (all[type] || []).slice().map(function (handler) {
    handler(evt);
  });
  (all["*"] || []).slice().map(function (handler) {
    handler(type, evt);
  });
}

订阅事件使用 on 方法,发布事件使用 emit 方法。emit 方法里就做了一件事,根据事件类型,将该类型订阅的所有事件遍历调用。

不足

通过分析 mitt 的源码,我们会发现,mitt 没有考虑匿名函数情况,在使用 on 方法时,传入的第二个参数必须是具名函数。

手动实现发布订阅库

class PubSub {
  constructor() {
    this.listeners = [];
  }

  sub(type, handler, always) {
    console.log(this.listeners[type] || []);
    if (!this.listeners[type]) {
      this.listeners[type] = [];
    }
    this.listeners[type].push({ handler, always });
  }

  on(type, handler, always = true) {
    this.sub(type, handler, always);
  }

  once(type, handler, always = false) {
    this.sub(type, handler, always);
  }

  emit(type, evt) {
    if (this.listeners[type]) {
      this.listeners[type].forEach((listener) => {
        listener.handler(evt);
      });

      this.listeners[type] = this.listeners[type].slice().filter((listener) => Boolean(listener.always));
    }
  }

  off(type, handler) {
    if (this.listeners[type]) {
      this.listeners[type] = this.listeners[type]
        .slice()
        .filter((listener) => listener.handler.toString() !== handler.toString());
    }
  }
}

输出是最有价值的输入

33 声望
8 粉丝
0 条评论
推荐阅读
「多图预警」完美实现一个@功能
一天产品大大向 boss 汇报完研发成果和产品业绩产出,若有所思的走出来,劲直向我走过来,嘴角微微上扬。产品大大:boss 对我们的研发成果挺满意的,balabala...(内心 OS:不听,讲重点)产品大大:咱们的客服 I...

wuwhs40阅读 4.8k评论 5

封面图
ESlint + Stylelint + VSCode自动格式化代码(2023)
安装插件 ESLint,然后 File -> Preference-> Settings(如果装了中文插件包应该是 文件 -> 选项 -> 设置),搜索 eslint,点击 Edit in setting.json

谭光志34阅读 20.8k评论 9

安全地在前后端之间传输数据 - 「3」真的安全吗?
在「2」注册和登录示例中,我们通过非对称加密算法实现了浏览器和 Web 服务器之间的安全传输。看起来一切都很美好,但是危险就在哪里,有些人发现了,有些人嗅到了,更多人却浑然不知。就像是给门上了把好锁,还...

边城32阅读 7.3k评论 5

封面图
涨姿势了,有意思的气泡 Loading 效果
今日,群友提问,如何实现这么一个 Loading 效果:这个确实有点意思,但是这是 CSS 能够完成的?没错,这个效果中的核心气泡效果,其实借助 CSS 中的滤镜,能够比较轻松的实现,就是所需的元素可能多点。参考我们...

chokcoco24阅读 2.3k评论 3

在前端使用 JS 进行分类汇总
最近遇到一些同学在问 JS 中进行数据统计的问题。虽然数据统计一般会在数据库中进行,但是后端遇到需要使用程序来进行统计的情况也非常多。.NET 就为了对内存数据和数据库数据进行统一地数据处理,发明了 LINQ (L...

边城17阅读 2k

封面图
过滤/筛选树节点
又是树,是我跟树杠上了吗?—— 不,是树的问题太多了!🔗 相关文章推荐:使用递归遍历并转换树形数据(以 TypeScript 为例)从列表生成树 (JavaScript/TypeScript) 过滤和筛选是一个意思,都是 filter。对于列表来...

边城18阅读 7.8k评论 3

封面图
Vue2 导出excel
2020-07-15更新 excel导出安装 {代码...} src文件夹下新建一个libs文件夹,新建一个excel.js {代码...} vue页面中使用 {代码...} ===========================以下为早期的文章今天在开发的过程中需要做一个Vue的...

原谅我一生不羁放歌搞文艺14阅读 20.1k评论 9

输出是最有价值的输入

33 声望
8 粉丝
宣传栏