第8期:mitt、tiny-emitter 发布订阅

804 阅读4分钟

第8期:mitt、tiny-emitter 发布订阅

一、学习前言

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

二、学习目标

2.1 了解 mitt、tiny-emitter的作用和应用场景

2.2 学习 发布订阅 设计模式

2.3 发布订阅模式 和 观察者模式 的异同点

三、环境准备

3.1 mitt的github地址:传送门

3.2 tiny-emitter的github地址:传送门

四、学习开始

4.1 先了解mitt和tiny-emitter是什么

vue3.x官方文档中讲到:从实例中完全移除了 $on 、$off 和 $once 方法,事件总线模式可以替换为使用外部的、实现了事件触发器接口的库,例如 mitt 或 tiny-emitter。vue3.x传送门

mitt和tiny-emitter提供了与vue2相同的事件触发器API。触发事件的监听及响应。

4.2 解读mitt源码

4.2.1 暴露的API
  • mitt: 发布订阅事件的发射器,以下API皆在当前返回的object中调用

  • all: 事件中心 存储所有的事件名称和事件对应的回调方法

    // Map对象存储
    // 写一个例子 注册了两个事件 say run
    // say中对应两个不同的监听回调方法handleSay1 handleSay2
    // run中对应的监听回调方法handleRun1
    const mapObj = new Map()
    mapObj.set('say', [handleSay1, handleSay2])
    mapObj.set('run', [handleRun1])
    
    • 清除所有的事件监听
      emitter.all.clear()
    
  • on: 事件监听 可分为两种方式监听

    • 监听具体的事件
    // listen to an event
    emitter.on('say', e => console.log('say', e) )
    
    • 监听所有事件
    // listen to all events
    emitter.on('*', (eventsType, e) => console.log(eventsType, e) )
    
  • off: 事件取消监听 也分为两种

    • 取消具体某一事件监听
    //unlisten to an event
    emitter.off('say', handleSay1) // 取消订阅say的handleSay1事件
    emitter.off('say') // 取消订阅say的所有事件
    
    • 取消全部的监听,配合emitter.on('*')使用
    // unlisten to all events
    emitter.off('*', (eventsType, e) => console.log(eventsType, e))
    
  • emit: 发布一个事件

    // fire an event
    emitter.emit('say', '我开始说话了')
    
4.2.2 源码API分析

我此处将ts转换为js,清晰一些

  • emit 方法
/**
 * Invoke all handlers for the given type.
 * If present, `'*'` handlers are invoked after type-matched handlers.
 *
 * Note: Manually firing '*' handlers is not supported.
 *
 * @param {string|symbol} type The event type to invoke
 * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler
 * @memberOf mitt
 */
emit (type, evt) {
  // 事件中心all 是用 new Map 存储的对象,所以先查一下是否注册过某事件
  let handlers = all.get(type);
  if (handlers) { // 如果已经被注册过,
    handlers
      .slice()
      .map((handler) => {
        handler(evt); // 则将注册过的事件重新分发一次
      });
  }
  handlers = all.get('*'); // 如果有全局监听的事件,也将所有事件中心注册的事件分发一次
  if (handlers) {
    handlers
      .slice()
      .map((handler) => {
        handler(type, evt);
      });
  }
}
  • on 方法
/**
 * Register an event handler for the given type.
 * @param {string|symbol} type Type of event to listen for, or `'*'` for all events
 * @param {Function} handler Function to call in response to given event
 * @memberOf mitt
 */
on (type, handler) {
  // 要添加新的监听,需要先查一下事件中心是否已有相对应的事件
  let handlers = all.get(type);
  if (handlers) { // 如果已经被注册过
    handlers.push(handler); // 向当前事件列表中继续添加事件监听回调
  }
  else { // 如果没有被注册过,则通过set新增一组事件对象
    all.set(type, [handler]);
  }
},

  • off 方法
/**
 * Remove an event handler for the given type.
 * If `handler` is omitted, all handlers of the given type are removed.
 * @param {string|symbol} type Type of event to unregister `handler` from (`'*'` to remove a wildcard handler)
 * @param {Function} [handler] Handler function to remove
 * @memberOf mitt
 */
off (type, handler) {
  // 同样的要去事件中心查询一下,有哪些订阅的方法组
  let handlers = all.get(type);
  if (handlers) {
    if (handler) { // 取消具体的某一个事件监听
      handlers.splice(handlers.indexOf(handler) >>> 0, 1);
    }
    else { // 取消当前事件type的所有监听事件 emitter.off('say')
      all.set(type, []);
    }
  }
},

扩展:>>> 无符号右移运算符

5 >>> 0     // 5
-1 >>> 0    // 4294967295
3.5 >>> 0   // 3
NaN >>> 0   // 0
// 返回的永远是正整数
const array = [1,2,3,4]
array.splice(-1 >>> 0, 1) // []   array = [1,2,3,4]
array.splice(3 >>> 0, 1)  // [4]  array = [1,2,3]
4.2.3 总结
  • emit 传递参数受限,不能够传递多个参数,多参数需要合并成object传递
  • 注册事件后需要手动的去取消事件,暂时没有$once只触发一次的方法
  • 源码是用TypeScript编写,总体代码120行,压缩后只有200k,轻便易理解。可以学习用TypeScript编写一个类型安全的小型插件库
  • 可以通过npm进行安装,也可以直接UMD构建引入,方便

4.3 解读tiny-emitter源码

4.3.1 暴露的API
  • on(event, callback[, context]): 订阅事件
    • event: 要订阅的事件的名称
    • callback: 发出事件时要调用的函数
    • context: (可选)- 要将事件回调绑定到的上下文
  • once(event, callback[, context]): 仅订阅一次事件
    • event: 要订阅的事件的名称
    • callback: 发出事件时要调用的函数
    • context: (可选)- 要将事件回调绑定到的上下文
  • off(event[, callback]): 取消订阅一个事件或所有事件。如果未提供回调,则会取消订阅所有事件
    • event: 要取消订阅的活动的名称
    • callback: 绑定到事件时使用的函数
  • emit(event[, arguments...]): 触发事件
    • event: 要触发的事件名称
    • arguments...: 要传递给事件订阅者的任意数量的参数
4.3.2 源码API分析
  • on
on: function (name, callback, ctx) {
  // 获取事件处理函数,第一次不存在则赋值为空
  var e = this.e || (this.e = {});

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

  return this; // 支持链式调用
},
  • once
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);
},
  • emit
emit: function (name) {
  var data = [].slice.call(arguments, 1); // arguments 类数组转化为数组,并且截取掉arguments的第一个参数,第一个参数指向事件类型,后面是执行需要的参数
  // 获取name事件处理函数组
  var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
  var i = 0;
  var len = evtArr.length;

  for (i; i < len; i++) {
    // evtArr[i] => {fn:callback, ctx:ctx}
    evtArr[i].fn.apply(evtArr[i].ctx, data);
  }

  return this;
},
  • off
off: function (name, callback) {
  var e = this.e || (this.e = {});
  var evts = e[name]; // 获取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) // 将不匹配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 // 只保留callback没有匹配到的函数
    : delete e[name];   // 如果没有传入callback,清除所有的name类型事件

  return this;
}

4.4 发布/订阅模式理解

4.4.1 理解

发布/订阅模式:有一个事件中心来记录所有的事件类型和事件类型处理函数组的映射对象。on用来注册事件,并将事件存储到事件中心去。emit派发事件后,相应触发事件中心中注册过的事件类型对应的事件函数组。off用来注销事件,将事件中心中匹配到的事件类型对应的(函数组中的事件)或(函数组)进行清空注销。once则是事件执行一次,即事件处理函数执行后进行事件的注销操作。

4.4.2 自己写一个简单的发布订阅模式
  class Subscribes {
    constructor() {
      this.eventList = new Map() // 订阅中心
    }
    // 访问订阅中心注册的所有事件
    get all() {
      return this.eventList
    }
    /**
     * @description: 注册事件
     * @param {String} eventName  // 事件类型 require
     * @param {Function} handleFn // 处理事件 require
     */
    on(eventName, handleFn) {
      const handlers = this.eventList.get(eventName)
      if (handlers) {
        // 如果此事件类型已注册,则在处理事件函数组中增加处理事件
        handlers.push(handleFn)
      } else {
        // 没有则重新添加
        this.eventList.set(eventName, [handleFn])
      }
    }
    /**
     * @description: 触发一次事件
     * @param {String} eventName  // 事件类型 require
     * @param {Function} handleFn // 处理事件 require
     */
    once(eventName, handleFn) {
      const _this = this
      function listener() {
        this.apply(handleFn, arguments)
        // 触发事件后,注销事件
        _this.off(eventName, listener)
      }
      this.on(eventName, listener)
    }
    /**
     * @description: 派发事件
     * @param {String} eventName  // 事件类型 require
     */
    emit(eventName) {
      const data = [].splice.call(arguments, 1) // 多参数处理
      const handlers = this.eventList.get(eventName)
      if (handlers) {
        // 处理事件函数组存在,则循环触发各处理事件
        for (let i = 0; i < handlers.length; i++) {
          handlers[i].apply(handlers[i], data)
        }
      }
    }
    /**
     * @description: 注销事件
     * @param {String} eventName  // 事件类型 require
     * @param {Function} handleFn // 处理事件 not require
     */
    off(eventName, handleFn) {
      if (!this.eventList.get(eventName)) return

      let handlers = this.eventList.get(eventName)
      // 如果传了具体的注销事件,则注销单个
      if (handleFn) {
        for (let i = 0; i < handlers.length; i++) {
          if (handlers[i] === handleFn) {
            handlers.splice(i, 1)
          }
        }
      } else {
        this.eventList.set(eventName, []);
      }
    }
  }

  const subscribe = new Subscribes()
  subscribe.on('study', studyLanguage)
  subscribe.on('study', studyMath)
  subscribe.once('study', studyOnce)

  function studyLanguage() {
    const data = Array.from(arguments)
    console.log(data.join('、') + ' 上了语文课');
  }
  function studyMath() {
    const data = [].slice.call(arguments)
    console.log(data.join('、') + ' 上了数学课');
  }
  function studyOnce() {
    const data = [].slice.call(arguments)
    console.log(data.join('、') + ' 上了只开设一节的体育课');
  }

  setTimeout(() => {
    subscribe.off('study', studyLanguage)
    console.log('-----语文停课了-----');
  }, 5000)

  let num = 0
  let timer = setInterval(() => {
    const students = ['小明', '小红', '小李', '张三', '李四', '王五']
    const chosenSon = students.slice(Math.floor(Math.random() * 5))
    console.log('随机抽选一批学生上课:' + chosenSon.join('、'));
    subscribe.emit('study', ...chosenSon)
    num++
    if (num > 5) clearInterval(timer)
  }, 2000)

4.5 观察者模式学习

4.5.1 定义

指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

4.5.2 理解

观察者模式:有一个用来维护所有观察者的对象也可称为被观察者列表,该对象可以自由添加观察者,可以自由移除观察者。被观察者状态要发生变化时,以广播的形式通知到列表中所有的观察者并自动更新。

4.5.3 写一个简单的订阅者模式
  // 维护观察者列表
  class Subject {
    constructor() {
      this.observerList = []
    }
    // 添加观察者
    addOb(observer) {
      if (observer && observer.update) {
        this.observerList.push(observer)
      }
    }
    // 通知所有观察者
    notify() {
      this.observerList.forEach(ob => {
        ob.update.apply(ob, arguments)
      })
    }
    // 移除观察者
    remove(observer) {
      const index = this.observerList.indexOf(observer)
      if (index > -1) {
        this.observerList.splice(index, 1)
      }
    }
  }

  // 观察者对象
  class Observer {
    constructor(name) {
      this.name = name
    }
    update(type) {
      console.log(this.name + type);
    }
  }

  const subject = new Subject()
  const observer1 = new Observer('小明')
  const observer2 = new Observer('小红')
  subject.addOb(observer1)
  subject.addOb(observer2)
  subject.notify('去吃饭了')
  subject.remove(observer1) // 观察者1移除
  subject.notify('放学了')

五、学习总结

插件 相同点 不同点
mitt 都有Api:on、emit、off (注册、派发、注销) 1.获取所有事件:可以通过all属性获取到事件类型和事件处理函数的映射对象,还可以一次性注销所有事件all.clear()
2."*":支持通配符事件,可实现全局注册,全局注销
3.emit: 传递参数受限,不能够传递多个参数,多参数需要合并成object传递
4.返回mitt对象,通过对象属性调用API
tiny-emitter 1.通过 e 可以获取到事件类型和事件处理函数的映射对象
2.支持设置执行上下文This,并且支持链式调用
3.emit:支持多参数多类型传参
4.返回函数实例,通过实例原型方法调用API

描述发布/订阅者模式观察者模式
概述发布者并不维护订阅者,也不知道订阅者的存在,所以也不会直接通知订阅者,而是通知调度中心,由调度中心通知订阅者。把观察者对象维护在目标对象中的,需要发布消息时直接发消息给观察者。在观察者模式中,目标对象本身是知道观察者存在的。
耦合性不存在耦合松耦合
角色发布者、订阅者、调度中心观察者、被观察者

欢迎指正交流