【若川视野 x 源码共读】第8期 | 从 mitt、tiny-emitter 浅析观察者模式源码

88 阅读4分钟

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

本期共读的两个模块mitt和tiny-emitter是对观察者模式的实现。

##首先来看看这个源码长什么样子

tiny-emitter源码地址

(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.TinyEmitter = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ },{}]},{},[1])(1)});

不懂就问,上面这个是什么东西?

1.什么是模块化?——避免变量污染和命名冲突的容器,并提供对外接口。

2.有啥好处?——拥有独立作用域,避免变量污染、命名冲突 - 降低项目复杂度,提高开发效率 - 提升代码可复用性和可维护性。


3.1 AMD

AMD(Asynchronous Module Definition),异步模块定义。在加载模块以及模块所依赖的其它模块时,AMD都采用异步加载的方式,避免模块加载阻塞网页渲染。

AMD作为一个规范,只需定义其语法API,而不关心其实现。AMD规范简单到只有一个API,即模块定义define函数:

define(name?, [dependencies]?, factory) - name是模块的标识,如果未提供则以文件名为标识; - dependencies表示所依赖的模块; - factory是模块初始化要执行的函数或对象,如果是函数,只执行一次,如果是对象,即为模块输出值。

AMD规范主要是针对前端浏览器的规范。

3.2 Commonjs

CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

CommonJS规范加载模块是同步加载的,也就是说,只有加载完成,才能执行后面的操作。

模块定义语法: module.exports=factory()

CommonJS主要是针对浏览器之外的js环境的规范。

这是一个匿名自执行函数,这个匿名函数拥有独立的作用域,既避免污染外界代码,也避免被外界代码污染。

展开第一个function可以看到如下代码:

typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
      (global = global || self, global.Vue = factory());

这段代码的作用是检测当前的运行环境支持哪种模块化规范,然后将Vue模块化,其中exports是Commonjs规范,define是AMD的规范。

在源码中可以看到,factory()最后return的是一个Vue对象。

当检测出为Commonjs规范时,导出Vue对象的写法为:

module.exports = factory()

当检测出为AMD规范时,写法为:

define(factory)

为了更直观,改写一下这段代码

if(typeof exports === 'object' && typeof module !== 'undefined'){
    // Commonjs规范
    module.exports = factory();
}else if(typeof define === 'function' && define.amd){
    // AMD规范
    define(factory);
}else{
    // 其他情况下,将Vue挂载到全局对象中
    // 在浏览器里就是给Window对象添加Vue属性,属性值为factory()返回的Vue对象
    global = global || self,
    global.Vue = factory();
}

c622b4760f4c01e75b5b87c127811f7b_1.jpg

我们回到主题让我们来试验下 tiny-emitter 怎么用

要不先简单理解下发布订阅吧,容我去找找

  1. 啥叫订阅 ?举个例子就是 vue 当中一个button绑定 @subscribe="subscribeDeal" 事件,另一种写法是v-on:click 大家想必不陌生吧,这里找个on 大家就可以理解为订阅啦,只不过on的这个时间不叫click叫subscribe,subscribe这个单词就是订阅的意思。
  2. 谁特么是发布 ? 继续看vue例子,上面我们订阅了一个叫subscribe事件,那么我们发布是不是 this.$emit("subscribe",value) 就可以不停发布了?
    methods:{
        triggerSend(){this.$emit("subscribe",value)}
    }
    
  1. 发布订阅好理解,但是什么是调度中心呢?我们直接看源码吧

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;
  },
  // 单次订阅,有没有想起vue的修饰符once  例: click.self.once
  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;
  }
};

看到这大家知道调度中心就是这个E函数的实例啦,每个实例的小e就是订阅的存储位置。