Backbone源码浅读:

前言:

Backbone是早起的js前端MV*框架之一,是一个依赖于underscore和jquery的轻量级框架,虽然underscore中基于字符串拼接的模板引擎相比如今基于dom元素双向绑定的模板引擎已显得落伍,但backbone作为引领前端mv*开发模式的先驱之一,依然是麻雀虽小却设计精妙,了解其设计与结构对于想一探mv*框架的初学者来说仍会获益匪浅。

Backbone结构:

Backbone分为几个部分:其中最核心的是Event事件模块,提供了实现事件与观察者模式的基础;随后是Model与Collection,提供了数据(Model)层面的抽象;接着是View,提供了数据与表现的相互链接,其模板引擎依赖于underscore的template方法;随后的async模块一直是我对Backbone又爱又恨的地方,一方面在代码层面的实现绑架了前后端的通信方式(虽然可以override),但另一方面这里数据与通信的模式又具备了Flux的雏形。最后是Router与History。当然除此之外也有extend,noConflict这样的技术辅助函数。总体而言,Backbone的代码小巧,结构清晰,易读易懂,对于初学者切入mv*框架非常适合。

Event模块:

Event模块将暴露以下api:on,off,once,trigger,这4个是我们所熟悉的事件模块/观察者模式的基本api;还有就是listenTo,stopListening,listenToOnce,这是前3个api的反向控制(Ioc)版本。对于两者我们可以这样理解:前者是站着被观察者的角度,需要暴露的api;而后者是站在观察者的角度所需要的api。这样设计的好处是,观察者在调用api进行观察(调用listenTo或listenToOnce)时,在自身保留与观察事物的索引,于是在观察者被销毁时,可以方便的注销自身在被观察者上已注册的回调(通过调用stopListening),从而避免泄露。

在event模块中首先定义的的是eventsApi函数,这一函数的功能是调用传入的iteratee,并传入events,name,callback和opts。

Iteratee是一个执行实际功能的函数,events是一个用来挂载所有事件回调的object,name是事件名称,callback是回调函数,opts则是额外参数。

我们可以看到eventsApi的作用其实只是封装对name的多态性处理,name可以是含有以多个事件名为键的object,可以是空格分隔事件名的的字符串,或是单一事件名的字符串。

var eventsApi = function(iteratee, events, name, callback, opts) {

  var i = 0, names;

  if (name && typeof name === 'object') {

    if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;

    for (names = _.keys(name); i < names.length ; i++) {

      events = eventsApi(iteratee, events, names[i], name[names[i]], opts);

    }

  } else if (name && eventSplitter.test(name)) {

    for (names = name.split(eventSplitter); i < names.length; i++) {

      events = iteratee(events, names[i], callback, opts);

    }

  } else {

    events = iteratee(events, name, callback, opts);

  }

  return events;

};

既然真正的核心是这些传入eventsApi的iteratee,那就让我们来看看这些iteratee以及如何使用它们形成最后的Api:

首先是onApi,这个函数的作用是通过传入的name,callback,在events对象上创建一个键为name(事件名)的数组,并将包含callback和context(回调函数触发时的上下文)以及listening对象的对象推入数组。这里的context和listening是options中取得的,listening(观察)对象中包含了反向控制时所需的信息,将在之后介绍。

var onApi = function(events, name, callback, options) {

  if (callback) {

    var handlers = events[name] || (events[name] = []);

    var context = options.context, ctx = options.ctx, listening = options.listening;

    if (listening) listening.count++;

    handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening });

  }

  return events;

};

onApi的进一步封装是internalOn。internalOn调用eventsApi并将onApi作为传入的iteratee。同时如果传入了listening对象,则将在被观察者上记录观察者和观察的信息。

var internalOn = function(obj, name, callback, context, listening) {

  obj._events = eventsApi(onApi, obj._events || {}, name, callback, {

      context: context,

      ctx: obj,

      listening: listening

  });

  if (listening) {

    var listeners = obj._listeners || (obj._listeners = {});

    listeners[listening.id] = listening;

  }

  return obj;

};

有了这些我们就能来实现on和listenTo了:

on只需简单的调用internalOn:

Events.on = function(name, callback, context) {

  return internalOn(this, name, callback, context);

};

而listenTo需要额外处理的便是,建立这个listening对象。Listening对象包含被观察者obj,用于在观察者上记录观察的listeningTo对象,以及计数器count。同一对观察者与被观察者之间的listen对象会被重用,在onApi调用时这个count便会+1来起到计数的作用。同时我们看到观察者和被观察者都会通过_.uniqueId函数产生的唯一id来标识自身。

Events.listenTo =  function(obj, name, callback) {

  if (!obj) return this;

  var id = obj._listenId || (obj._listenId = _.uniqueId('l'));

  var listeningTo = this._listeningTo || (this._listeningTo = {});

  var listening = listeningTo[id];

  if (!listening) {

    var thisId = this._listenId || (this._listenId = _.uniqueId('l'));

    listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};

  }

  internalOn(obj, name, callback, this, listening);

  return this;

};

我们已经知道了在被观察者上的事件回调是{eventKey1: [...], eventKey2: [...], ...}的格式,因此回调的移除便是通过便利这个_events对象来实现的,需要注意的是,移除侦听时name和callback都是可以缺省的,没有callback则移除_events[name]中包含的所有回调,如果连name都没有则移除所有侦听,这两者在例如观察者会被观察者整个销毁时非常实用。

var offApi = function(events, name, callback, options) {

  if (!events) return;

  var i = 0, listening;

  var context = options.context, listeners = options.listeners;

  if (!name && !callback && !context) {

    var ids = _.keys(listeners);

    for (; i < ids.length; i++) {

      listening = listeners[ids[i]];

      delete listeners[listening.id];

      delete listening.listeningTo[listening.objId];

    }

    return;

  }

  var names = name ? [name] : _.keys(events);

  for (; i < names.length; i++) {

    name = names[i];

    var handlers = events[name];

    if (!handlers) break;

    var remaining = [];

    for (var j = 0; j < handlers.length; j++) {

      var handler = handlers[j];

      if (

        callback && callback !== handler.callback &&

          callback !== handler.callback._callback ||

            context && context !== handler.context

      ) {

        remaining.push(handler);

      } else {

        listening = handler.listening;

        if (listening && --listening.count === 0) {

          delete listeners[listening.id];

          delete listening.listeningTo[listening.objId];

        }

      }

    }

    if (remaining.length) {

      events[name] = remaining;

    } else {

      delete events[name];

    }

  }

  if (_.size(events)) return events;

};

这里的remaining数组避免了反复调用slice。同时我们注意到移除侦听时既要比对handler.callback也要比对handler.callback._callback,后者是因为once绑定侦听时使用了包裹过的函数,其_callback指向原函数。

off和stopListening的实现也水到渠成:

Events.off =  function(name, callback, context) {

  if (!this._events) return this; // 还没有被侦听,直接返回

  this._events = eventsApi(offApi, this._events, name, callback, {

      context: context,

      listeners: this._listeners

  });

  return this;

};

Events.stopListening =  function(obj, name, callback) {

  var listeningTo = this._listeningTo;

  if (!listeningTo) return this; // 还没有侦听,直接返回

  var ids = obj ? [obj._listenId] : _.keys(listeningTo);

  for (var i = 0; i < ids.length; i++) {

    var listening = listeningTo[ids[i]];

    if (!listening) break; //还没有对被观察者的侦听,直接返回

    listening.obj.off(name, callback, this); // 调用被观察者的Events.off

  }

  if (_.isEmpty(listeningTo)) this._listeningTo = void 0;

  return this;

};

接下来的iteratee是onceMap,它会把原本的callback包装为once,并在once上记录下原callback,以便方便移除侦听(外部只知道侦听了callback,无需了解这个once的存在)。 _.once所包裹生成的函数将保证原函数只会被调用一次,once调用时用offer(name, once)移除这个单次侦听。

var onceMap = function(map, name, callback, offer) {

  if (callback) {

    var once = map[name] = _.once(function() {

      offer(name, once);

      callback.apply(this, arguments);

    });

    once._callback = callback;

  }

  return map;

};

once和listenToOnce 便很直接了:

Events.once =  function(name, callback, context) {

  var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));

  return this.on(events, void 0, context);

};

Events.listenToOnce =  function(obj, name, callback) {

  var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));

  return this.listenTo(obj, events);

};

最后的trigger也无需多言,值得注意的是triggerEvents 中判断了args的长度再调用call,是因为Function#apply的效率较低,在args长度可以预判的情况下尽量使用call,这是一个常见的小技巧。

var triggerApi = function(objEvents, name, cb, args) {

  if (objEvents) {

    var events = objEvents[name];

    var allEvents = objEvents.all;

    if (events && allEvents) allEvents = allEvents.slice();

    if (events) triggerEvents(events, args);

    if (allEvents) triggerEvents(allEvents, [name].concat(args));

  }

  return objEvents;

};

var triggerEvents = function(events, args) {

  var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];

  switch (args.length) {

    case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;

    case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;

    case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;

    case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;

    default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;

  }

};

Events.trigger =  function(name) {

  if (!this._events) return this;

  var length = Math.max(0, arguments.length - 1);

  var args = Array(length);

  for (var i = 0; i < length; i++) args[i] = arguments[i + 1];

  eventsApi(triggerApi, this._events, name, void 0, args);

  return this;

};

最后就是在开头加上:

var Events = Backbone.Events = {}; //将Events挂到Backbone上

var eventSplitter = /\s+/; // 事件名可以用空格分隔

结尾加上:

// bind和unbind是on和off的别名

Events.bind   = Events.on;

Events.unbind = Events.off;

// Backbone本身也挂载了Events的api。

_.extend(Backbone, Events);

这样Backbone的Events模块便完成了。

最新文章

  1. BizTalk动手实验(五)Map开发测试
  2. linux学习笔记 2013-09-02
  3. hiho 第118周 网络流四&#183;最小路径覆盖
  4. Android小项目之九 两种上下文的区别
  5. 解析php时间戳与日期的转换
  6. jquery 60秒倒计时(方法二)
  7. HDU 5636 Shortest Path 分治+搜索剪枝
  8. sea.js总结
  9. ps中套索工具怎么使用的方法
  10. baba 运动网
  11. iOS 为label添加删除线
  12. mybatis运行时拦截ParameterHandler注入参数
  13. 关于js代码执行顺序
  14. MySQL如何判别InnoDB表是独立表空间还是共享表空间
  15. 使用js修改url地址参数
  16. BZOJ4241 历史研究 莫队 堆
  17. gulp-px2rem-plugin 插件的一个小bug
  18. c#继承中的函数调用实例
  19. 电信版华为MATE7 EMUI4.0回退3.1和3.0教程与中转包
  20. Mysql创建用户并授权以及开启远程访问

热门文章

  1. vm10虚拟机安装Mac OS X10.10教程
  2. mac 剪切文件
  3. 17.4.3 使用MulticastSocket实现多点广播(1)
  4. jquery 如何动态添加、删除class样式方法介绍_jquery_脚本之家
  5. ComboBox值排序
  6. C++异常(exception)第三篇-------如何释放资源(没有finally语句)
  7. poj3190区间类贪心+优先队列
  8. ubuntu如何安装Mac主题
  9. (中等) HDU 1828 Picture,扫描线。
  10. iOS调用另一个程序