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. HDU 2897 邂逅明下(巴什博奕变形)

    巴什博奕的变形,与以往巴什博奕不同的是,这里给出了上界和下界,原先是(1,m),现在是(p,q),但是原理还是一样的,解释如下: 假设先取者为A,后取者为B,初始状态下有石子n个,除最后一次外其他每次 ...

  2. Linq to SQL 简单的增删改操作

    Linq to SQL 简单的增删改操作. 新建数据库表tbGuestBook.结构如下: 新建web项目,完成相应的dbml文件.留言页面布局如下 <body> <form id= ...

  3. tinkphp5.0 traits 的引入

    Traits引入 ThinkPHP 5.0开始采用trait功能(PHP5.4+)来作为一种扩展机制,可以方便的实现一个类库的多继承问题. trait是一种为类似 PHP 的单继承语言而准备的代码复用 ...

  4. js优化原则

    首先,与其他语言不同,JS的效率很大程度是取决于JS engine的效率.除了引擎实现的优劣外,引擎自己也会为一些特殊的代码模式采取一些优化的策略.例如FF.Opera和Safari的JS引擎,都对字 ...

  5. 如何创建一个要素数据类 IField,IFieldEdit,IFields,IFieldsEditI,GeometryDef,IGeometryDefEdit接口

    如何创建一个要素数据类 创建要素类用到了IFeatureWorkspace.CreateFeatureClass方法,在这个方法中有众多的参数,为了满足这些参数,我们要学习和了解下面的接口. IFie ...

  6. iOS WebView的用法

    一.UIWebView 可以加载和显示某个URL的网页,也可以显示基于HTML的本地网页或部分网页: a. 加载 URL WebView = [[UIWebView alloc] initWithFr ...

  7. CentOS6.6部署OpenStack Havana(Nova-Network版)

    CentOS6.4部署OpenStack Havana(Nova-Network版) 一 基本设备介绍 测试环境 CentOS6.4 x64 OpenStack 服务 介绍 计算 (Compute) ...

  8. Express 简介

    Express 简介 Express 是一个简洁而灵活的 node.js Web应用框架, 提供了一系列强大特性帮助你创建各种 Web 应用,和丰富的 HTTP 工具. 使用 Express 可以快速 ...

  9. (简单) POJ 3254 Corn Fields,状压DP。

    Description Farmer John has purchased a lush new rectangular pasture composed of M by N (1 ≤ M ≤ 12; ...

  10. 关于Discuz与jQuery冲突问题的亲测解决方法

    最近的一个项目整合dede和discuz程序,客户要求风格统一,所以有很多样式及特效都是要公用的.其中jQuery库定义的函数$()正好与discuz的comme.js中函数一样,这样就冲突了,导致d ...