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模块便完成了。

Backbone源码解读(一)事件模块的更多相关文章

  1. 虎说:bootstrap源码解读(重置模块)

    ------<!--action-->------ 开场show:前不生“不犹豫”,后半生“不后悔”.今天又逃课,我不后悔 素材:推特公司的前端框架bootstrap(下称bt),解读源码 ...

  2. BackBone 源码解读及思考

    说明 前段时间略忙,终于找到时间看看backbone代码. 正如知友们说的那样,backbone简单.随性. 代码简单的看一眼,就能知道作者的思路.因为简单,所以随性,可以很自由的和其他类库大搭配使用 ...

  3. jquery源码分析(七)——事件模块 event(二)

    上一章节探讨了事件的一些概念,接下来看下jQuery的事件模块. jQuery对事件的绑定分别有几个API:.bind()/.live()/.delegate()/.on()/click(), 不管是 ...

  4. Webpack探索【16】--- 懒加载构建原理详解(模块如何被组建&如何加载)&源码解读

    本文主要说明Webpack懒加载构建和加载的原理,对构建后的源码进行分析. 一 说明 本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack懒加载构建原理. 本文使用的 ...

  5. Webpack探索【15】--- 基础构建原理详解(模块如何被组建&如何加载)&源码解读

    本文主要说明Webpack模块构建和加载的原理,对构建后的源码进行分析. 一 说明 本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack的基础构建原理. 本文使用的W ...

  6. Abp 审计模块源码解读

    Abp 审计模块源码解读 Abp 框架为我们自带了审计日志功能,审计日志可以方便地查看每次请求接口所耗的时间,能够帮助我们快速定位到某些性能有问题的接口.除此之外,审计日志信息还包含有每次调用接口时客 ...

  7. seajs 源码解读

    之前面试时老问一个问题seajs 是怎么加载js 文件的 在网上找一些资料,觉得这个写的不错就转载了,记录一下,也学习一下 seajs 源码解读 seajs 简单介绍 seajs是前端应用模块化开发的 ...

  8. SDWebImage源码解读之SDWebImagePrefetcher

    > 第十篇 ## 前言 我们先看看`SDWebImage`主文件的组成模块: ![](http://images2015.cnblogs.com/blog/637318/201701/63731 ...

  9. Alamofire源码解读系列(一)之概述和使用

    尽管Alamofire的github文档已经做了很详细的说明,我还是想重新梳理一遍它的各种用法,以及这些方法的一些设计思想 前言 因为之前写过一个AFNetworking的源码解读,所以就已经比较了解 ...

随机推荐

  1. Service Activity三种交互方式

    Service Activity三种交互方式 2012-09-09 22:52 4013人阅读 评论(2) 收藏 举报 serviceandroidimportclassthreadjava     ...

  2. Mac os 10.11 更新ruby

    1.装cocoapods,ruby版本忒低->开始更新ruby->开始更新gem,这是一条不归路啊同志们,各种permission denied,各种路径不存在,各种路径没有读写权限,各种 ...

  3. libevent和libev的区别对比(二)

    之前有一篇文章描述过一些对比: http://www.cnblogs.com/charlesblc/p/6078029.html 这里在代码和应用方面再说一下. 看一下两边的Helloworld基本就 ...

  4. python之requests模块

    1.安装 pip install requests 2.基本用法 就是以某种HTTP方法向远端服务器发送一个请求而已 import requests r = requests.get('https:/ ...

  5. mac home/end/pageup/pageDown

    home:按fn+左键 end:按fn+右键 pageup:按fn+上键 pagedown:按fn+下键

  6. -linux删除大量文件----rm,rsync

    要在linux下删除海量文件,比如有数十万个文件,此时常用的rm -rf * 就会等待时间很长.这时我们可以使用rsync快速删除大量文件. 1.建立一个空目录 mkdir -p /tmp/rsync ...

  7. maven bundle

    今天引入了几个bundle到pom,尽然说missing,我还以为是nexus组织下载.把type=bundle去掉可以下载,后来同事给了我这个连接https://issues.apache.org/ ...

  8. AVR单片机的BOOT区

    BOOT区的由来基于一个简单的道理,即单片机的程序是保存在FLASH中的,要运行程序就必须不停的访问FLASH存储器.对于一般的FLASH存储器,数据的写入需要一定的时间来完成,在数据写入完成之前,存 ...

  9. Android Material各种颜色设置

    Blogpost about support appcompat v21 from Chris Banes

  10. 12、手把手教你Extjs5(十二)执行菜单命令在tabPanel中显示模块

    上面设计好了一个模块的主界面,下面通过菜单命令的执行来把这个模块加入到主界面当中.在MainModule.js中有一个函数,生成了当前的菜单数据: // 根据data.systemMenu生成菜单条和 ...