EventProxy 源码分析

对朴大的eventproxy的解读。

上来就把要用的一些通用方法,抽离出来。

 var SLICE = Array.prototype.slice;
 var CONCAT = Array.prototype.concat;
 var later = (typeof setImmediate !== 'undefined' && setImmediate) ||
   (typeof process !== 'undefined' && process.nextTick) || function (fn) {
   setTimeout(fn, 0);
};

其中later是异步的作用,这里是根据不同的环境来确定later的,
如果有setImmediate就用setImmediate这个nodejs给我们的异步函数,如果没有就用process.nextTick这个nodejs的loop函数,如果实在没有就用timer这个比较消耗资源的setTimeout。

一些常量的定义

var ALL_EVENT = '__all__';

这个ALL_EVENT是个event名称(代表所有的event都会触发的事件名称)

eventproxy的构造函数

  var EventProxy = function () {
    // 这个考虑到了如果把这个constuctor当做普通的函数调用, 那么也返回 new EventProxy
    if (!(this instanceof EventProxy)) {
      return new EventProxy();
    }
  // 这个用于记录event事件和callback关系的数据结构
    this._callbacks = {};
  // 在all或tail方法中存取相应订阅的data
    this._fired = {};
  };

这里先提前说一下,其实eventproxy主要就是订阅/发布设计模式的实现。
那么下面的那个方法就是核心的方法之一

订阅

  EventProxy.prototype.addListener = function (ev, callback) {
    // 就是往_callbacks上边根据事件名称,绑定方法
    this._callbacks[ev] = this._callbacks[ev] || [];
    this._callbacks[ev].push(callback);
    return this;
  };

下面是addListener的一些alias和扩展

  /**
   * `addListener` alias, `bind`
   */
  EventProxy.prototype.bind = EventProxy.prototype.addListener;
  /**
   * `addListener` alias, `on`
   */
  EventProxy.prototype.on = EventProxy.prototype.addListener;
  /**
   * `addListener` alias, `subscribe`
   */
  EventProxy.prototype.subscribe = EventProxy.prototype.addListener;
  // 订阅所有的事件
  EventProxy.prototype.bindForAll = function (callback) {
    this.bind(ALL_EVENT, callback);
  };

和addListener方法相似的另一个方法,和addListener主要区别这个方法是把订阅的函数unshift到相应事件array的头部,而不是push到array的尾部。
下一个方法是removeListener,移除订阅。

   // 用于移除事件的method
  EventProxy.prototype.removeListener = function (eventname, callback) {
    var calls = this._callbacks;
    // 如果没有事件的名称,就把所有的事件全部clear掉
    if (!eventname) {
      this._callbacks = {};
    } else {
      // 如果没有指定callback的话,把该事件所有的callback都一起删掉
      if (!callback) {
        debug('Remove all listeners of %s', eventname);
        calls[eventname] = [];
      } else {
      // 如果指定了callback,有递归一下把该callback原来的位置 null
        var list = calls[eventname];
        if (list) {
          var l = list.length;
          for (var i = 0; i < l; i++) {
            if (callback === list[i]) {
              list[i] = null;
            }
          }
        }
      }
    }
    return this;
  };

移除订阅的一些alias和扩展

EventProxy.prototype.unbind = EventProxy.prototype.removeListener;

EventProxy.prototype.removeAllListeners = function (event) {
   return this.unbind(event);
};

EventProxy.prototype.unbindForAll = function (callback) {
   this.unbind(ALL_EVENT, callback);
};

发布

  EventProxy.prototype.trigger = function (eventname, data) {
    var list, ev, callback, i, l;
    var both = 2;
    var calls = this._callbacks;
    debug('Emit event %s with data %j', eventname, data);
    while (both--) {
      // 1 为 eventname 0 为 ALL_EVENT
      ev = both ? eventname : ALL_EVENT;
      list = calls[ev];
      if (list) {
        for (i = 0, l = list.length; i < l; i++) {
          // 如果callback = nil 去掉
          // unbind掉的callback
          if (!(callback = list[i])) {
            list.splice(i, 1);
            i--;
            l--;
          } else {
            var args = [];
            var start = both ? 1 : 0;
            for (var j = start; j < arguments.length; j++) {
              args.push(arguments[j]);
            }
            callback.apply(this, args);
          }
        }
      }
    }
    return this;
  };

这里注意会先触发eventName这个事件的所有订阅函数,然后触发ALL_EVENT的所有订阅函数。
发布函数的alias们

EventProxy.prototype.emit = EventProxy.prototype.trigger;
EventProxy.prototype.fire = EventProxy.prototype.trigger;

其实上面这几个方法已经实现了基础的订阅/发布者模式。
下面是一些扩展,也是解决callback痛点的主要部分。

once订阅

  EventProxy.prototype.once = function (ev, callback) {
    var self = this;
    // wrapper一下,添加一个unbind
    //一个对高阶函数的应用
    var wrapper = function () {
      callback.apply(self, arguments);
      self.unbind(ev, wrapper);
    };
    this.bind(ev, wrapper);
    return this;
  };

这个其实就是对高阶函数的一个应用,wrapper一下callback添加一些逻辑,这里是添加了self.unbind(ev, wrapper)这个逻辑。

emitLater

异步发布事件,利用了前边介绍的later方法,代码如下:

  EventProxy.prototype.emitLater = function () {
    var self = this;
    var args = arguments;
    later(function () {
      self.trigger.apply(self, args);
    });
  };

immediate 绑定一个事件,然后立刻触发它

  EventProxy.prototype.immediate = function (ev, callback, data) {
    this.bind(ev, callback);
    this.trigger(ev, data);
    return this;
  };

  /**
   * `immediate` alias
   */
  EventProxy.prototype.asap = EventProxy.prototype.immediate;

all和tail方法

all和tail的helper

  // 用于绑定all和tail的helper method
  var _assign = function (eventname1, eventname2, cb, once) {
    var proxy = this;
    // 参数的个数
    var argsLength = arguments.length;
    // 判断被回调的次数
    var times = 0;
    // 标记是否执行过
    var flag = {};

    // 参数肯定是大于三的
    // Check the arguments length.
    if (argsLength < 3) {
      return this;
    }
    // 事件 arr
    var events = SLICE.call(arguments, 0, -2);
    // 回调函数
    var callback = arguments[argsLength - 2];
    // 是否是一次
    var isOnce = arguments[argsLength - 1];

    // Check the callback type.
    if (typeof callback !== "function") {
      return this;
    }

    debug('Assign listener for events %j, once is %s', events, !!isOnce);

    // 用于绑定事件的helper
    var bind = function (key) {
      var method = isOnce ? "once" : "bind";
      proxy[method](key, function (data) {
        proxy._fired[key] = proxy._fired[key] || {};
        proxy._fired[key].data = data;
        if (!flag[key]) {
          flag[key] = true;
          times++;
        }
      });
    };

    // 绑定所有的事件
    var length = events.length;
    for (var index = 0; index < length; index++) {
      bind(events[index]);
    }

    // 这个是all_event的callback
    var _all = function (event) {
      // 没有全部执行完(不是所有的事件都执行完毕)
      if (times < length) {
        return;
      }

      // 如果这个事件没有执行结束,return
      if (!flag[event]) {
        return;
      }

      var data = [];
      for (var index = 0; index < length; index++) {
        data.push(proxy._fired[events[index]].data);
      }

      if (isOnce) {
        proxy.unbindForAll(_all);
      }

      debug('Events %j all emited with data %j', events, data);
      callback.apply(null, data);
    };

    proxy.bindForAll(_all);
  };

all调用这个helper用once为true,tail调用这个helper用once为false。

  EventProxy.prototype.all = function (eventname1, eventname2, callback) {
    var args = CONCAT.apply([], arguments);
    // push true 来确定是once
    args.push(true);
    _assign.apply(this, args);
    return this;
  };


  /**
   * `all` alias
   */
  EventProxy.prototype.assign = EventProxy.prototype.all;


  EventProxy.prototype.tail = function () {
    var args = CONCAT.apply([], arguments);
    // 不是一次的
    args.push(false);
    _assign.apply(this, args);
    return this;
  };

  /**
   * `tail` alias, assignAll
   */
  EventProxy.prototype.assignAll = EventProxy.prototype.tail;
  /**
   * `tail` alias, assignAlways
   */
  EventProxy.prototype.assignAlways = EventProxy.prototype.tail;

after方法

  EventProxy.prototype.after = function (eventname, times, callback) {
    if (times === 0) {
      callback.call(null, []);
      return this;
    }
    var proxy = this,
      // 这个用于存无序的after调用的结果
      firedData = [];
    this._after = this._after || {};

    var group = eventname + '_group';

    // 这个用于存取有序的group的结果
    this._after[group] = {
      index: 0,
      results: []
    };

    debug('After emit %s times, event %s\'s listenner will execute', times, eventname);
    var all = function (name, data) {
      if (name === eventname) {
        times--;
        firedData.push(data);
        if (times < 1) {
          debug('Event %s was emit %s, and execute the listenner', eventname, times);
          proxy.unbindForAll(all);
          callback.apply(null, [firedData]);
        }
      }
      // 如果是group来emit的,会保持emit的顺序,把数据放到_after[group].results中
      if (name === group) {
        times--;
        proxy._after[group].results[data.index] = data.result;
        if (times < 1) {
          debug('Event %s was emit %s, and execute the listenner', eventname, times);
          proxy.unbindForAll(all);
          callback.call(null, proxy._after[group].results);
        }
      }
    };
    proxy.bindForAll(all);
    return this;
  };

  EventProxy.prototype.group = function (eventname, callback) {
    var that = this;
    var group = eventname + '_group';
    var index = that._after[group].index;
    // 这个用于记录第几个emit
    that._after[group].index++;
    return function (err, data) {
      if (err) {
        // put all arguments to the error handler
        return that.emit.apply(that, ['error'].concat(SLICE.call(arguments)));
      }
      that.emit(group, {
        index: index,
        // callback(err, args1, args2, ...)
        result: callback ? callback.apply(null, SLICE.call(arguments, 1)) : data
      });
    };
  };

其中group方法是个特殊emit方法,保证了after订阅函数得到的数据的数组是有序的。

any方法

  EventProxy.prototype.any = function () {
    var proxy = this,
      callback = arguments[arguments.length - 1],
      events = SLICE.call(arguments, 0, -1),
      _eventname = events.join("_");

    debug('Add listenner for Any of events %j emit', events);
    proxy.once(_eventname, callback);

    var _bind = function (key) {
      proxy.bind(key, function (data) {
        debug('One of events %j emited, execute the listenner');
        proxy.trigger(_eventname, {"data": data, eventName: key});
      });
    };

    for (var index = 0; index < events.length; index++) {
      _bind(events[index]);
    }
  };

错误处理 fail/throw

  EventProxy.prototype.fail = function (callback) {
    var that = this;

    that.once('error', function () {
      that.unbind();
      // put all arguments to the error handler
      // fail(function(err, args1, args2, ...){})
      callback.apply(null, arguments);
    });
    return this;
  };

  /**
   * A shortcut of ep#emit('error', err)
   */
  EventProxy.prototype.throw = function () {
    var that = this;
    that.emit.apply(that, ['error'].concat(SLICE.call(arguments)));
  };

done方法

对错误处理的一层封装

  EventProxy.prototype.done = function (handler, callback) {
    var that = this;
    return function (err, data) {
      if (err) {
        // put all arguments to the error handler
        return that.emit.apply(that, ['error'].concat(SLICE.call(arguments)));
      }

      // callback(err, args1, args2, ...)
      var args = SLICE.call(arguments, 1);

      if (typeof handler === 'string') {
        // getAsync(query, ep.done('query'));
        // or
        // getAsync(query, ep.done('query', function (data) {
        //   return data.trim();
        // }));
        if (callback) {
          // only replace the args when it really return a result
          return that.emit(handler, callback.apply(null, args));
        } else {
          // put all arguments to the done handler
          //ep.done('some');
          //ep.on('some', function(args1, args2, ...){});
          return that.emit.apply(that, [handler].concat(args));
        }
      }

      // speed improve for mostly case: `callback(err, data)`
      if (arguments.length <= 2) {
        return handler(data);
      }

      // callback(err, args1, args2, ...)
      handler.apply(null, args);
    };
  };

总之,学习一下朴大的代码风格,和事件订阅和发布的实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,392评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,258评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,417评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,992评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,930评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,199评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,652评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,327评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,463评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,382评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,432评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,118评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,704评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,787评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,999评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,476评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,057评论 2 341

推荐阅读更多精彩内容