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

1. 前言

这次学习的项目是

  • mitt
  • tiny-emitter

    1.1 你能学到😎

  • 发布订阅模式

  • 了解 mitt、tiny-emitter 用法
  • 以及他们的应用场景
  • 具体实现

    2. 这个库,是干啥的😶

    老规矩,先看README

    mitt

    Tiny 200b functional event emitter / pubsub.

    • Microscopic: weighs less than 200 bytes gzipped
    • Useful: a wildcard “*” event type listens to all events
    • Familiar: same names & ideas as Node’s EventEmitter
    • Functional: methods don’t rely on this
    • Great Name: somehow mitt wasn’t taken

    Mitt was made for the browser, but works in any JavaScript runtime. It has no dependencies and supports IE9+.
    微型 200b 功能事件发射器 / pubsub。

    • 微观: gzip 压缩后的重量小于 200 字节
    • 有用:通配符”*”事件类型监听所有事件
    • 熟悉:与Node 的 EventEmitter相同的名称和想法
    • 函数式:方法不依赖this
    • 好名字:不知怎的,手套没有被拿走(这里应该是说该名字没有被别人用过

    Mitt 是为浏览器设计的,但适用于任何 JavaScript 运行时。它没有依赖项,支持 IE9+。

tiny-emitter

A tiny (less than 1k) event emitter library.
一个很小(小于 1k)的事件发射器库。

README中即可得知这两个库是用于发布订阅(上面谷歌翻译的发射,应该是发布)以及监听

2. 发布订阅模式🤔

这个所谓的发布订阅以及监听,就有点像是我们常用的onClick事件等监听~,但不局限于此~

我们为什么需要这个模式

  1. 需要向多个地方广播信息
    1. 具体点就比如登录成功之后,很多地方就不要再提示登录了而是提供登录后的个人信息等
  2. 需要广播信息,但是并不需要回应

    图解

    发布订阅模式有三个模块:

  3. 发布者

  4. 订阅者
  5. 调度中心

image.png
看起来非常的直观、简单,那就让我们来看看是怎么实现的


3 看看 源码👻

  1. 了解API
  2. 猜测实现
  3. 看源码

    3.1 mitt

    我们先看 mitt

    API

    ```javascript 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

  1. 很明显,就是导出了一个`mitt`函数,调用后返回一个`emitter`对象,对象上有这几个属性/方法:
  2. - `on(type, handler)`:进行事件的订阅
  3. - `off(type, [handler])`:取消订阅给定类型的`handler`
  4. - `emit`:调用给定类型的所有`handler`
  5. - `all`:存储事件类型和事件处理函数的映射Map
  6. <a name="vafax"></a>
  7. ### 源码
  8. 源码在 [mitt/src/index.ts](https://github.com/developit/mitt/blob/main/src/index.ts),是ts的,希望大家都会😏如果不会,可以先编译一下为js,不过其实看ts代码时都没什么难的,就是类型而已<br />这里就是一些ts的接口,定义参数类型什么的,不是重点
  9. ```typescript
  10. export type EventType = string | symbol;
  11. // An event handler can take an optional event argument
  12. // and should not return a value
  13. export type Handler<T = unknown> = (event: T) => void;
  14. export type WildcardHandler<T = Record<string, unknown>> = (
  15. type: keyof T,
  16. event: T[keyof T]
  17. ) => void;
  18. // An array of all currently registered event handlers for a type
  19. export type EventHandlerList<T = unknown> = Array<Handler<T>>;
  20. export type WildCardEventHandlerList<T = Record<string, unknown>> = Array<WildcardHandler<T>>;
  21. // A map of event types and their corresponding event handlers.
  22. export type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
  23. keyof Events | '*',
  24. EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
  25. >;
  26. export interface Emitter<Events extends Record<EventType, unknown>> {
  27. all: EventHandlerMap<Events>;
  28. on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
  29. on(type: '*', handler: WildcardHandler<Events>): void;
  30. off<Key extends keyof Events>(type: Key, handler?: Handler<Events[Key]>): void;
  31. off(type: '*', handler: WildcardHandler<Events>): void;
  32. emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
  33. emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void;
  34. }

重点是下面的mitt方法,(为了减少篇幅,我删除了一些原文中的英文注释),详细中文注释如下:

  1. //导出mitt函数,调用后返回一个Emitter对象
  2. export default function mitt<Events extends Record<EventType, unknown>>(
  3. all?: EventHandlerMap<Events>//?: 表示all是可选参数
  4. ): Emitter<Events> {
  5. type GenericEventHandler =
  6. | Handler<Events[keyof Events]>
  7. | WildcardHandler<Events>;
  8. all = all || new Map(); //支持传入all参数,如果不传那就new Map一个赋值给all
  9. return {
  10. //存储事件类型和事件处理函数的映射Map
  11. all,
  12. //on函数注册事件,type为类型,handler为处理函数,存储在handlers数组中
  13. //用数组是因为可能监听一个事件,有多个处理函数
  14. on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
  15. const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
  16. if (handlers) {//有值直接放入
  17. handlers.push(handler);
  18. }
  19. else {//无值就初始化
  20. //type为属性值,handlers数组为属性值
  21. //!.是TS断言,意思是all中必有set这个东东
  22. all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
  23. }
  24. },
  25. //取消某个事件的回调函数
  26. off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {
  27. //根据属性名type取对应的属性值handlers处理函数数组
  28. const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
  29. if (handlers) {
  30. if (handler) {//如果有传入hanlder
  31. //就找到对应的位置,并将其从数组中删除,这个>>>是个亮点后文详细写一下
  32. handlers.splice(handlers.indexOf(handler) >>> 0, 1);
  33. }
  34. else {//没有传入handler就删除全部
  35. all!.set(type, []);
  36. }
  37. }
  38. },
  39. //根据type找到对应的事件处理函数数组并全部执行
  40. emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
  41. //根据属性名type找到属性值handlers
  42. let handlers = all!.get(type);
  43. if (handlers) {
  44. (handlers as EventHandlerList<Events[keyof Events]>)
  45. .slice()//生成新数组,使得后面的map不会改变原数组
  46. .map((handler) => {//依次执行处理函数,并传入对应的参数
  47. handler(evt!);
  48. });
  49. }
  50. //获取监听全部事件的处理函数,type事件∈全部事件~
  51. handlers = all!.get('*');
  52. if (handlers) {//同上
  53. (handlers as WildCardEventHandlerList<Events>)
  54. .slice()
  55. .map((handler) => {
  56. handler(type, evt!);
  57. });
  58. }
  59. }
  60. };
  61. }

Map

Map 是一个带键的数据项的集合,就像一个 Object 一样。 但是它们最大的差别是 Map 允许任何类型的键(key),如果你还不了解其API,可以点击链接查看。

all简洁地建立映射

  1. if (handlers) {
  2. if (handler) {//如果有传入hanlder
  3. //就找到对应的位置,并将其从数组中删除,这个>>>是个亮点后文详细写一下
  4. handlers.splice(handlers.indexOf(handler) >>> 0, 1);
  5. }
  6. else {//没有传入handler就删除全部
  7. all!.set(type, []);
  8. }
  9. }

利用Map存储对象的方式是引用,简洁地完成了有值取值无值初始化的效果
image.png

无符号右移(>>>)

  1. >>>运算符执行无符号右移位运算。
  2. 它把无符号的 32 位整数所有数位整体右移。
  3. 对于无符号数或正数右移运算,无符号右移与有符号右移运算的结果是相同的
  4. 对于负数来说,无符号右移将使用 0 来填充所有的空位

indexOf如果找不到对应的下标,是会返回一个 -1 的,而 -1 在slice中是有效的,即倒数第一个,很明显将不符合要求
image.png
-1>>>0后会返回一个很大的数 4294967295,不会修改到原数组,而正数>>>0后不会改变

3.2 tiny-emitter

整体上大同小异~

API

  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');
  • on(name, callback, ctx)
  • once(name, callback, ctx):只订阅一次事件
  • emit(name)
  • off(name, callback)

多了一个once,从接收参数来看,还多了一个ctx即执行上下文~

源码

直接看源码~ 在 tiny-emitter/index.js
(为了减少篇幅,我删除了一些原文中的英文注释),详细中文注释如下:

  1. //定义函数E
  2. function E () {
  3. //使其为空让其很容易继承
  4. }
  5. //在原型上修改
  6. E.prototype = {
  7. //在原型对象上建立方法
  8. on: function (name, callback, ctx) {
  9. //获取处理函数和事件的映射——用对象存储,获取为空就初始化为{}空对象
  10. var e = this.e || (this.e = {});
  11. //经典简洁的有值取值,无值初始化
  12. (e[name] || (e[name] = [])).push({
  13. fn: callback,
  14. ctx: ctx
  15. });
  16. return this;
  17. },
  18. once: function (name, callback, ctx) {
  19. var self = this;//防止里面的函数this丢失
  20. function listener () {
  21. self.off(name, listener);//注销订阅
  22. callback.apply(ctx, arguments);//执行处理函数
  23. };
  24. listener._ = callback//存储在_中,方便后面off时查找
  25. //将封装之后的事件处理函数作为on参数之一 返回一个on
  26. return this.on(name, listener, ctx);
  27. },
  28. emit: function (name) {
  29. //用arguments对象获取传入的全部参数
  30. var data = [].slice.call(arguments, 1);
  31. //获取name类型对应的处理函数数组
  32. var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
  33. var i = 0;
  34. var len = evtArr.length;
  35. //for循环遍历执行
  36. for (i; i < len; i++) {
  37. evtArr[i].fn.apply(evtArr[i].ctx, data);
  38. }
  39. return this;
  40. },
  41. off: function (name, callback) {
  42. var e = this.e || (this.e = {});
  43. var evts = e[name];
  44. var liveEvents = [];//注销后有效的处理函数数组
  45. if (evts && callback) {
  46. for (var i = 0, len = evts.length; i < len; i++) {
  47. //fn._ 就是once处理过的
  48. if (evts[i].fn !== callback && evts[i].fn._ !== callback)
  49. liveEvents.push(evts[i]);
  50. }
  51. }
  52. //处理函数数组为空的话就要删除,不为空的话就更新到e数组中
  53. (liveEvents.length)
  54. ? e[name] = liveEvents
  55. : delete e[name];
  56. return this;
  57. }
  58. };
  59. module.exports = E;
  60. module.exports.TinyEmitter = E;

支持链式调用

return this 实现支持链式调用

4. 学习资源

  • Map

    4. 总结 & 收获👩‍🍳

    两库之同

  • 都非常小,但功能也算齐全,缺陷的话就是用js的时候像传参什么的没有报错,当然用ts的话就能补足这个缺陷了

  • 这两个库实现发布订阅模式的方式大同小异~
    • 使用一个结构存储事件类型与处理函数的映射,通过该映射操作类型对应的处理函数:
      • 调用
      • 取消订阅
  • API 基本也相同

    • on 订阅
    • off 取消订阅
    • emit 调用

      两库之不同

  • 一个是用 Map 一个使用对象{}

    • ps:好像mitt以前版本也是用对象的
  • emit
    • 支持'*'订阅全部事件
    • all 获取全部事件
    • 返回一个用函数生成的对象,属性都是来自自身
  • tiny-emitter

    • 支持链式调用
    • 可以从e中获取全部事件,我觉得应该用稍微语义化一点的API…,另外这个在 README里也没说,看了源码才知道
    • 多一个once方法,实现只订阅一次
    • 返回的是函数实例,属性都是来自原型
    • 上一次更新是在19年了,很多代码都是很老的写法:
      • var而不是constlet
      • for遍历而不是map
      • 下一步读源码展望😎

  • 发现 developit是个宝藏开源项目开发者啊~ 后续可以多读读他的库嘿嘿,看起来都很小而美,并且不少与react相关的

    🌊如果有所帮助,欢迎点赞关注,一起进步⛵