likes
comments
collection
share

【若川视野 x 源码共读】第8期 | mitt、tiny-emitter 发布订阅

作者站长头像
站长
· 阅读数 75

前言

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

什么是发布订阅

发布订阅模式,对于前端开发来说,并不陌生,在我们刚接触前端开发的时候,我们就已经开始接触到其雏形了,addEventListenerremoveEventListener,给某给DOM元素绑定事件。Vue中的$on$emit方法,也是采用了发布订阅模式,我们一起来探索下其实现方法。

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。

订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

探索发布订阅

从字面意思上看,我们知道该模式主要分为两个部分,即发布订阅,我们通过on来进行事件的订阅,通过emit来进行事件的发布,同时我们还需要一个调度中心event来存储订阅。

我们通过Mitt类来实现,我们先来定义一下其雏形。

// 定义Mitt类
class Mitt{
  // 调度中心
  event = {};
  // 订阅
  on() {};
  // 发布
  emit() {};
}
​

订阅

接下来,我们先来完成事件订阅吧,我们来回忆下订阅过程

// 订阅使用方法
on("save", () => {console.log("save")})

我们知道,订阅需要两个参数事件名name和事件callback,我们很容易实现订阅

// 订阅实现方法
on(name, callback) {
  this.event[name] = callback;
}

我们可能会订阅多次,这时我们会发现一个问题,上述方法并不满足,后续的订阅会覆盖掉之前的订阅,所以我们需要通过一个数组来存储所有的订阅,我们改进一下。

// 订阅实现方法
on(name, callback) {
  // 判断是否存在当前订阅,若不存在,初始化为[]
  const event = this.event[name] || (this.event[name] = []);
  // 存储当前订阅
  event.push(callback);
}

我们来测试一下订阅

const mitt = new Mitt();
​
mitt.on("save", () => console.log("save1"));
mitt.on("save", () => console.log("save2"));
mitt.on("del", () => console.log("del"))
console.log(mitt.event);
/*
{
  save: [
    () => console.log("save1"),
    () => console.log("save1")
  ],
  del: [
    () => console.log("del")
  ]
}
*/

发布

实现完订阅,我们先来完成事件发布吧,事件发布过程很简单

// 发布使用方法
emit("save")

我们只需要通过emit发布事件name

// 发布实现方法
emit(name) {
  // 获取订阅的事件集合
  const events = this.event[name];
  // 如果存在,遍历执行每一个订阅
  if(events) {
    events.forEach(event => {
      event();
    })
  }
};

我们来测试一下发布

mitt.emit("save");
// save1 save2

自此我们便简单实现了订阅发布,相对来说还是比较简单的,将复杂的事情简单化,有助于我们去理解。

我们除了订阅发布外,我们知道,还存在取消订阅

取消订阅

取消订阅的过程,就将之前的订阅删除,通过off来进行取消订阅

// 取消订阅实现方法
off(name) {
  // 如果传了特定的订阅,则清除其相关的订阅,否则清除所有的订阅
  if(name) {
    this.event[name] = [];
  } else {
    this.event = {};
  }
}

我们来测试一下取消发布

mitt.emit("del"); // del
mitt.off("del");
mitt.emit("del"); // 未触发订阅

mitt源码解析

通过闭包来使用

function mitt(all) {
  all = all || new Map();
  
  return  {
    all,
    on (type, handler) {
      // 查找 map中是否有这个key
      const handlers = all.get(type);
      // 存在就直接push
      if (handlers) {
        handlers.push(handler);
      } else {
        // 不存在就创建 value是一个数组
        all.set(type, [handler]);
      }
    },
    off (type, handler) {
      const handlers = all.get(type);
      if (handlers) {
        if (handler) {
          // 删除指定的 handler处理函数, 找到了 idx >>> 0 就是idx对应的索引
          // 没找到 -1 变为 4294967295,原数组不会改变
          handlers.splice(handlers.indexOf(handler) >>> 0, 1);
        } else {
          // 如果没有传对应的handler,则把此type的全部事件全部清除
          all.set(type, [])
        }
      }
    },
    emit (type, evt) {
      let handlers = all.get(type);
      if (handlers) {
        handlers.slice().map((handler) => {
          handler(evt);
        })
      }
      debugger;
      // 如果存在监听*的情况,则会将事件全部触发一次
      handlers = all.get('*');
      if (handlers) { 
        handlers.slice().map((handler) => { 
          handler(type, evt); 
        });
      }
    }
  }
}

看源码后,比较迷惑的是*订阅所有的事件,实际触发的还是订阅*的事件。

tiny-emitter源码解析

通过实例化E类来使用

function E () {}
​
E.prototype = {
  // 订阅
  on: function (name, callback, ctx) {
    // 创建变量e,即事件调度中心,通过普通对象存储
    var e = this.e || (this.e = {});
    // 存储订阅事件
    (e[name] || (e[name] = [])).push({
      fn: callback,
      ctx: ctx
    });
​
    return this;
  },
  // 单次订阅
  once: function (name, callback, ctx) {
    var self = this;
    function listener () {
      // 主要实现逻辑就是在触发事件的时候,取消当前订阅
      self.off(name, listener);
      callback.apply(ctx, arguments);
    };
​
    listener._ = callback
    // 调用on订阅事件
    return this.on(name, listener, ctx);
  },
  // 发布
  emit: function (name) {
    // 获取callback函数的运行参数
    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;
  },
  // 取消订阅
  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]);
      }
    }
    // 如果存在其他订阅事件,则修改原存储的事件,否则直接删掉原存储事件
    (liveEvents.length)
      ? e[name] = liveEvents
      : delete e[name];
​
    return this;
  }
};

总结

理解了发布订阅模式的概念和实现原理,自己对于源码的阅读也更加轻松。

在取消订阅时,之前也了解过,通过订阅事件生成唯一id来进行定向取消订阅,感兴趣的可以自己尝试一下。

转载自:https://juejin.cn/post/7182583235786833977
评论
请登录