backbone源码解读

写在前面

backbone是我两年多前入门前端的时刻接触到的第一个框架,当初被backbone的壮大功用所吸收(固然确实比裸写js要好许多),虽然现在backbone并不算最主流的前端框架了,然则,它内里大批设想情势的天真运用,以及使人赞叹的处置惩罚技能,照样异常值得进修。个人以为,读懂老牌框架的源代码比会用盛行框架的API要有用的多。

别的,backbone的源代码近来也改了许多(特别是针对ES6),所以有些老旧的剖析,能够会和现在的源代码有些相差。

所以我写这一篇剖析backbone的文章,供本身和人人一同进修,本文合适运用过backbone的朋侪,笔者水平有限,而内容又实有点多,难免会出差错,迎接人人在GitHub上斧正

接下来,我们将经由历程一篇文章剖析backbone,我们是根据源码的递次来解说的,这有利于人人边看源代码边解读,别的,我给源代码加了悉数的中文诠释和讲明,请见这里,强烈发起人人边看源码边看剖析,而且碰到我给出外链的处所,最好把外链的内容也看看(假如能够给人人协助,迎接给star勉励~)

固然,这篇文章很长[为了防备文章有上没下,我照样整合到一篇文章中了]。

backbone宏观解读

backbone是很初期将MVC的头脑带入前端的框架,现在MVC以及厥后的MVVM这么火能够在一定水平上归功于backbone。关于前端MVC,我在本身的这篇文章中连系阮一峰先生的图示简朴剖析过,简朴来讲就是Model层掌握数据,View层经由历程宣布定阅(在backbone中)来处置惩罚和用户的交互,Controller是掌握器,在这里主如果指backbone的路由功用。如许的设想异常直接清楚,有利于前端工程化。

backbone中主要完成了Model、Collection、View、Router、History几大功用,前四种我们用的比较多,别的backbone基于宣布-定阅情势本身完成了一套对象的事宜体系Events,简朴来讲Events能够让对象具有事宜才能,其定义了比较丰富的API,而且假如你引入了backbone,这套事宜体系还能够集成到本身的对象上,这是一个异常好的设想。

别的,源代码中一切的以_开首的要领,能够以为是私有要领,是没有必要直接运用的,也不发起用户掩盖。

backbone模块化处置惩罚、防备争执和underscore混入

代码起首举行了辨别运用环境(self或许是global,前者代表浏览器环境(self和window等价),后者代表node环境)和模块化处置惩罚操纵,以后处置惩罚了在AMD和CommonJS加载类型下的引入体式格局,而且明白声清楚明了对jQuery(或许Zepto)和underscore的依靠。

很遗憾的是,虽然backbone如许做了,然则backbone并不合适在node端直接运用,也不合适服务端衬着,别的还和ES6相处的不是很和谐,这个我们背面还会连续提到缘由。

backbone noConflict

backbone也向jQuery致敬,进修了它的处置惩罚争执的体式格局:

var previousBackbone = root.Backbone;
//...
Backbone.noConflict = function() {
    root.Backbone = previousBackbone;
    return this;
};

这段代码的逻辑异常简朴,我们能够经由历程以下体式格局运用:

var localBackbone = Backbone.noConflict();   
var model = localBackbone.Model.extend(...);

混入underscore的要领

backbone经由历程addUnderscoreMethods将一些underscore的有用要领混入到本身定义的几个类中(注:确实地说是可供组织挪用的函数,我们下文也会用类这个简朴清楚明了的说法替代)。

这内里值得一提的是关于underscore的要领(underscore的源码解读请移步这里,fork from韩子迟),underscore的一切要领的参数序列都是牢固的,也就是说第一个参数代表什么第二个参数代表什么,一切函数都是一致的,第一个参数一定代表目标对象,第二个参数一定代表作用函数(有的函数能够只要一个参数),在有三个参数的状况下,第三个参数代表上下文this,别的假如有第四个参数,第三个参数代表初始值或许默许值,第四个参数代表上下文。所以addMethod就是根据以上划定来运用的。

别的关于javascript中的this,我曾写过博客在这里,有兴致的能够看

混入要领的完成逻辑:

var addMethod = function(length, method, attribute) {
  //... 
};
var addUnderscoreMethods = function(Class, methods, attribute) {
    _.each(methods, function(length, method) {
      if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
    });
};
//以后运用:
var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
      omit: 0, chain: 1, isEmpty: 1};
//混入一些underscore中经常使用的要领
addUnderscoreMethods(Model, modelMethods, 'attributes');

backbone Events

backbone的Events是一个对象,个中的要领(onlistenTooffstopListeningoncelistenToOncetrigger)都是对象要领。

总体上,backbone的Events完成了监听/触发/消除对本身对象本身的事宜,也能够让一个对象监听/消除监听别的一个对象的事宜。

绑定对象本身的监听事宜on

关于对象本身事宜的绑定,这个比较简朴,除了最基础的绑定之外(一个事宜一个回调),backbone还支撑以下两种体式格局的绑定:

//传统体式格局
model.on("change", common_callback);  

//传入一个称号,回调函数的对象
model.on({ 
     "change": on_change_callback,
     "remove": on_remove_callback
});  

//运用空格支解的多个事宜称号绑定到同一个回调函数上
model.on("change remove", common_callback);  

这用到了它定义的一个中心函数eventsApi,这个函数比较有用,能够根据推断运用的是哪一种体式格局(现实上这个推断也比较简朴,根据传入的是对象推断属于上述第二种体式格局,根据正则表达式推断是上述的第三种体式格局,不然就是传统的体式格局)。然后再举行递归或许轮回或许直接处置惩罚。

在对象中存储事宜现实上或许是下述情势:

events:{
    change:[事宜一,事宜二]
    move:[事宜一,事宜二,事宜三]
}

而个中的事宜现实上是一个整顿好的对象,是以下情势:

{callback: callback, context: context, ctx: context || ctx, listening: listening}

如许在触发的时刻,一个个挪用就是了。

监听其他对象的事宜listenTo

backbone还支撑监听其他对象的事宜,比方,B对象上面发作b事宜的时刻,关照A挪用回调函数A.listenTo(B, “b”, callback);,而这也是backbone处置惩罚异常奇妙的处所,我们来看看它是怎样做的。

现实上,这和B监听本身的事宜,而且在回调函数的时刻把上下文变成A,是差不多的:B.on(“b”, callback, A);(on的第三个参数代表上下文)。

然则backbone还做了别的的事变,这里我们假定是A监听B的一个事宜(比方change事宜好了)。

起首A有一个A._listeningTo属性,这个属性是一个对象,存放着它监听的别的对象的信息A._listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0},这个id并非数字,是每个对象都有的唯一字符串,是经由历程_.uniqueId这个underscore要领天生的,这里的obj是B,objId是B的_listenId,id是A的_listenId,count是一个计数功用,而这个A._listeningTo[id]会被直接援用赋值到上面事宜对象的listening属性中。

为何要多listenTo?Inversion of Control

经由历程以上我们彷佛有一个疑问,彷佛on就能把listenTo的功用搞定了,用一个listenTo纯属过剩,而且许多其他的类库也是只要一个on要领。

起首,这里会引入一个观点:掌握反转,所谓掌握反转,就是本来这个是B对象来掌握的事宜我们现在交由A对象来掌握,那现在假定A离别listenTo B、C、D三个对象,那末这个时刻假定A不监听了,那末我们直接对A挪用一个stopListening要领,则能够同时消除对B、C、D的监听(这里我讲的能够不是非常准确,这里别的引荐一个文章)。

别的,我们须要从backbone的设想初志来看,backbone的重点是View、Model和Collection,现实上,backbone的View能够对应一个或许多个Collection,固然我们也能够让View直接对应Model,但题目是View也并不一定对应一个Model,能够对应多个Model,那末这个时刻我们经由历程listenTo和stopListening能够异常轻易的增添、消除监听。

//on的体式格局绑定
var view = {
    DoSomething :function(some){
       //...
    }
}
model.on('change:some',view.DoSomething,view);
model2.on('change:some',view.DoSomething,view);

//解绑,这个时刻要做的事变比较多且乱
model.off('change:some',view.DoSomething,view);
model2.off('change:some',view.DoSomething,view);

//listenTo的体式格局绑定
view.listenTo(model,'change:some',view.DoSomething);
view.listenTo(model2,'change:some',view.DoSomething);

//解绑
view.stopListening();

别的,在现实运用中,listengTo的写法也确实越发相符用户的习气.

以下是摘自backbone官方文档的一些诠释,仅供参考:

The advantage of using this form, instead of other.on(event, callback, object), is that listenTo allows the object to keep track of the events, and they can be removed all at once later on. The callback will always be called with object as context.

消除绑定事宜off、stopListening

与on差别,off的三个参数都是可选的

  • 假如没有任何参数,off相称于把对应的_events对象团体清空

  • 假如有name参数然则没有细致指定哪一个callback的时刻,则把这个name(事宜)对应的回调行列悉数清空

  • 假如另有进一步细致的callback和context,那末这个时刻移除回调函数异常严厉,必需要求上下文和本来函数完整一致

off的终究完成函数是offApi,这个函数算上诠释有或许50行。

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

这内里须要零丁提一下,前面有如许的几行:

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;
}

这几行是做了一件什么事呢?
删除了一切的多对象监听事宜纪录,以后删除本身的监听事宜。我们假定A监听了B的一个事宜,这个时刻A._listenTo中就会多一个条目,存储这个监听事宜的信息,而这个时刻B的B._listeners也会多一个条目,存储监听事宜的信息,注重这两个条目都是根据id为键的键值对来存储,然则这个键是不一样的,值都指向同一个对象,这里删除对这个对象的援用,以后就能够被渣滓接纳机制接纳了。假如这个时刻挪用B.off(),那末这个时刻,以上的两个条目都被删除了。别的,注重末了的return,以及Events.off中的:

this._events = eventsApi(offApi, this._events, name, callback, {
      context: context,
      listeners: this._listeners
});

所以假如B.off()如许挪用然后直接把 B._events 在以后也清空了,太奇妙了

以后有一个对names(事宜名)的轮回(假如没有指定,那末默许就是一切names),这个轮回内容明白起来比较简朴,内里也趁便照应了_listeners_listenTo这些变量。这里不过量诠释了。

别的,stopListening现实上也是挪用offApi,先处置惩罚了一下交给off函数,这也是设想情势运用模范(适配器情势)。

once和listenToOnce

这两个函数望文生义,和on以及listenTo的区分不大,唯一的区分就是回调函数只供挪用一次,多触发挪用也没有用(现实上不会被触发了)。

二者都用到了onceMap这个函数,我们剖析一下这个函数:

 var onceMap = function(map, name, callback, offer) {
    if (callback) {
      //_.once:竖立一个只能挪用一次的函数。反复挪用革新的要领也没有用果,只会返回第一次实行时的效果。 作为初始化函数运用时异常有用, 不必再设一个boolean值来搜检是不是已初始化完成.
      var once = map[name] = _.once(function() {
        offer(name, once);
        callback.apply(this, arguments);
      });
      //这个在解绑的时刻有一个区分效果
      once._callback = callback;
    }
    return map;
 };

backbone的设想思绪是如许的:用_.once()竖立一个只能被挪用一次的函数,这个函数在第一次被触发挪用的时刻,举行消除绑定(offer现实上是一个已绑定好this的消除绑定函数,这个能够拜见once和listenToOnce的源代码),然后再挪用callback,如许既完成了挪用一次的目标,也轻易了渣滓接纳。

其他和on以及listenTo的时刻一样,这里就不过量引见了。

trigger

trigger函数是用于触发事宜,支撑多个参数,除了第一个参数之外,其他的参数会顺次放入触发事宜的回调函数的参数中(backbone默许对3个参数及以下的状况下举行call挪用,这类处置惩罚体式格局缘由之一是call挪用比apply挪用的效力更高从而优先运用(关于call和apply的机能对照:https://jsperf.com/call-apply…),别的一方面源码中并没有凌驾三个参数的状况,所以用call支撑到了三个参数,其他状况采纳机能较差然则写起来轻易的apply)。

别的值得一提的是,Events支撑all事宜,即假如你监听了all事宜,那末任何事宜的触发都邑挪用all事宜的回调函数列。

关于trigger部份的源代码比较简朴,而且我也增添了一些评注,这里就不贴代码了。

context 和 ctx

故意的朋侪或许注重到,backbone在事宜顶用到了context和ctx这两个”貌似”示意当前上下文的对象,而且在假如有context的状况下,这两个险些一样:

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

这里我根据本身的明白,只管诠释一下。

我们能够主要看off要领及trigger要领,我们发明上面两属性在这两个要领中离别被运用了。

off里须要对context举行比较决议是不是要删除对应的事宜,所以model._events中保留下来的context,必需是未做修正的。

而trigger里在实行回调函数时,须要指定其作用域,当绑定事宜时没有给定作用域,则会运用被监听的对象当回调函数的作用域。

现实上,我以为这个ctx有点过剩,我们完整能够在trigger中如许写:

(ev = events[i]).callback.call(ev.context || ev.obj)

backbone Model

backbone的Model现实上是一个可供组织挪用的函数,backbone采纳污染原型的体式格局把定义好的属性都定义在了prototype上,这能够并非一个异常稳健的做法,然则在backbone中如许做倒是没有什么不能够的,这个我们在以后讲extend要领的时刻会举行补充。

我们先看看这个函数在实例化的时刻会做点什么:

 var Model = Backbone.Model = function(attributes, options) {
    var attrs = attributes || {};
    options || (options = {});
    //这个preinitialize函数现实上是为空的,能够给有兴致的开辟者重写这个函数,在初始化Model之前挪用
    this.preinitialize.apply(this, arguments);
    //Model的唯一的id
    this.cid = _.uniqueId(this.cidPrefix);
    this.attributes = {};
    if (options.collection) this.collection = options.collection;
    //假如以后new的时刻传入的是JSON,我们必需在options选项中声明parse为true
    if (options.parse) attrs = this.parse(attrs, options) || {};
    //_.result:假如指定的property的值是一个函数,那末将在object上下文内挪用它;不然,返回它。假如供应默许值,而且属性不存在,那末默许值将被返回。假如设置defaultValue是一个函数,它的效果将被返回。
    //这里挪用_.result相称于给出了余地,本身写defaults的时刻能够直接写一个对象,也能够写一个函数,经由历程return一个对象的体式格局把属性包括进去
    var defaults = _.result(this, 'defaults');
    //defaults应当是在Backbone.Model.extends的时刻由用户增添的,用defaults对象添补object 中的undefined属性。 而且返回这个object。一旦这个属性被添补,再运用defaults要领将不会有任何效果。
    attrs = _.defaults(_.extend({}, defaults, attrs), defaults);
    this.set(attrs, options);
    //存储汗青变化纪录
    this.changed = {};
    //这个initialize也是空的,给初始化以后挪用
    this.initialize.apply(this, arguments);
};

我们能够看出,this.attributes是存储现实内容的。

别的,preinitialize和initialize不仅在Model中有,在以后的Collection、View和Router中也都涌现了,一个是在初始化前挪用,别的一个是在初始化以后挪用。

关于preinitialize的题目,我们后文还要继承议论,它的涌现和ES6有关。

Model set

Model的set要领是一个重点的要领,这个要领的功用比较多,本身以至还能够删除属性,由于unset内部和clear的内部等也挪用了set要领。在用户手动赋值的时刻,支撑下面两种赋值体式格局:"key", value{key: value}两种赋值体式格局。

我们剖析这个函数统共做了哪些事变:

  • 对两种赋值体式格局的支撑"key", value{key: value}的预处置惩罚。

  • 假如你写了validate考证函数没有经由历程考证,那末就不继承做了(须要显式声明运用validate)。

  • 举行变量的变动或许删除,趁便把汗青版本的题目解决掉。

  • 假如不是寂静set的,那末这个时刻最先举行change事宜的触发。

细致这一块诠释笔者写的异常细致,所以在这里也不再赘述。

fetch、save、destroy

这几个功用是须要跟服务端交互的,所以我们放在一同来剖析一下。

backbone经由历程封装好模子和服务器交互的函数,大大轻易了开辟者和服务端数据同步的事情,固然,这须要一个对应的后端,不仅须要支撑POST、PUT、PATCH、DELETE、GET多种要求,以至连url的花样都给定义好了,url的花样为:yourUrl/id,这个id一定是须要我们传入的,而且要求跟服务器上的id对应(毕竟服务器要辨认处置惩罚)

注重:url并不一定非要根据backbone的来,我们完整能够挪用这几个要领的时刻再指定一个url{url:myurl,success:successFunction},这个部份backbone 在sync函数中举行了一个推断处置惩罚,优先遴选后指定的url,不过如许对我们来讲是比较贫苦的,也并不相符backbone的设想初志

这三个函数末了都用到了sync函数,所以我们要先剖析sync函数:

Backbone.sync = function(method, model, options) {
  //...
};
  
Backbone.ajax = function() {
  return Backbone.$.ajax.apply(Backbone.$, arguments);
};

sync函数在个中挪用了ajax函数,而ajax函数就是jQuery的ajax,这个我们异常熟习,它能够插进去异常多的参数,我们能够这里检察文档。

别的,这个sync支撑两个特殊状况:

  • emulateHTTP:假如你想在不支撑Backbone的默许REST/ HTTP体式格局的Web服务器上事情, 您能够遴选开启Backbone.emulateHTTP。 设置该选项将经由历程 POST 要领捏造 PUT,PATCH 和 DELETE 要求 用实在的要领设定X-HTTP-Method-Override头信息。 假如支撑emulateJSON,此时该要求会向服务器传入名为 _method 的参数。

  • emulateJSON:假如你想在不支撑发送 application/json 编码要求的Web服务器上事情,设置Backbone.emulateJSON = true;将致使JSON根据模子参数举行序列化, 并经由历程application/x-www-form-urlencoded MIME类型来发送一个捏造HTML表单要求

细致的这个sync要领,就是组织ajax参数的历程。

fetch

fetch能够传入一个回调函数,这个回调函数会在ajax的回调函数中被挪用,别的ajax的回调函数是在fetch中定义的,这个回调函数做了如许几件事变:

 options.success = function(resp) {
        //处置惩罚返回数据
        var serverAttrs = options.parse ? model.parse(resp, options) : resp;
        //根据服务器返回数据设置模子属性
        if (!model.set(serverAttrs, options)) return false;
        //触发自定义回调函数
        if (success) success.call(options.context, model, resp, options);
        //触发事宜
        model.trigger('sync', model, resp, options);
 };
save

save要领为向服务器提交保留数据的要求,假如是第一次保留,那末就是POST要求,假如不是第一次保留数据,那末就是PUT要求。

个中,通报的options中能够运用的字段以及意义为:

  • wait: 能够指定是不是守候服务端的返回效果再更新model。默许状况下不守候

  • url: 能够掩盖掉backbone默许运用的url花样

  • attrs: 能够指定保留到服务端的字段有哪些,合营options.patch能够发生PATCH对模子举行部份更新

  • patch:boolean 指定运用部份更新的REST接口

  • success: 本身定义一个回调函数

  • data: 会被直接通报给jquery的ajax中的data,能够掩盖backbone一切的对上传的数据掌握的行动

  • 其他: options中的任何参数都将直接通报给jquery的ajax,作为其options

关于save函数细致的处置惩罚逻辑,我在源代码中增添了异常细致的诠释,这里就不睁开了。

destroy

烧毁这个模子,我们能够剖析,烧毁模子要做以下几件事变:

  • 住手对该对象一切的事宜监听,本身都没有了,还监听什么事宜

  • 示知服务器本身要被烧毁了(假如isNew()返回true,那末实在不必向服务器发送要求)

  • 假如它属于某一个collection,那末要示知这个collection要把这个模子移除

个中,通报的options中能够运用的字段以及意义为:

  • wait: 能够指定是不是守候服务端的返回效果再烧毁。默许状况下不守候

  • success: 本身定义一个回调函数

Model的其他内容

别的值得一提的是,Model是要求传入的id唯一的,然则对这个id假如反复的状况下的错误处置惩罚做的不是很到位,所以有的时刻你看掌握台报错并不能实时发明题目。

backbone Collection

Collection也是一个可供组织挪用的函数,我们照样先看看这个Collection做了些什么:

var Collection = Backbone.Collection = function(models, options) {
    options || (options = {});
    this.preinitialize.apply(this, arguments);
    //现实上我们在竖立鸠合类的时刻大多数都邑定义一个model, 而不是在初始化的时刻从options中指定model
    if (options.model) this.model = options.model;
    //我们能够在options中指定一个comparator作为排序器
    if (options.comparator !== void 0) this.comparator = options.comparator;
    //_reset用于初始化
    this._reset();
    this.initialize.apply(this, arguments);
    //假如我们在new组织挪用的时刻声清楚明了models,这个时刻须要挪用reset函数
    if (models) this.reset(models, _.extend({silent: true}, options));
  };

现实上,我以为backbone的Model、View、Collection里的逻辑照样比较清楚的,可读性也比较强,所以主要就是把诠释写在代码内里。

Collection set

collection的一个中心要领,内容很长,我们能够把它明白为重置:给定一组新的模子,增添新的,去除不在这内里的(在增添情势下不去除),夹杂已存在的。然则这个要领同时也很天真,能够经由历程参数的设定来转变情势

set能够有以下几个挪用场景:

  1. 重置情势,这个时刻不在models里的model都邑被清撤除。对应上文的:var setOptions = {add: true, remove: true, merge: true};

  2. 增添情势,这个时刻models里的内容会做增添用,假如有反复的(cid来推断),会掩盖。对应上文的:var addOptions = {add: true, remove: false};

我们照样理一理内里做了哪些事变:

  • 先类型化models和options两个参数

  • 遍历models:

    • 假如是重置情势,那末碰到反复的就直接掩盖掉,而且也增添到set行列,碰到新的就先增添到set行列。以后还要删撤除models里没有而本来collection内里有的

    • 假如是增添情势,那末碰到反复的,就先增添到set行列,碰到新的也是增添到set行列

  • 以后举行整顿,整合到collection中(能够会触发排序操纵)

  • 假如不是寂静处置惩罚,这个时刻会触发各种事宜

固然,我们在举行挪用的时刻,是不须要斟酌这么庞杂的,这个函数之所以做的这么庞杂,是由于它也供许多内置的其他函数挪用了,如许能够削减反复代码的冗余,相符函数式编程的头脑。别的set函数虽然冗杂却不赘余,内里定义的函数内变量逻辑都有本身的作用。

sort

上文中提到了sort函数,sort所根据的是用户传入的comparator参数,这个参数能够是一个字符串示意的单个属性也能够是一个函数,别的也能够是一个多个属性构成的数组,假如是单个属性或许函数,就挪用underscore的排序要领,假如是一个多个属性构成的数组,就挪用原生的数组排序要领(原生要领支撑根据多个属性分优先级举行排序)

fetch、create

这是Collection中涉及到和服务端交互的要领,这两个要领异常有区分。

fetch是直接从服务器拉取数据,并没有挪用model的fetch要领,返回的数据花样应当是直接能够挪用上文的set函数的数据花样,别的值得注重的是,想要挪用这个要领,一定要先指定url

create是指将特定的model上传到服务器上去,并没有挪用本身的要领而是末了挪用了model本身的要领model.save(null, options),这里第一个参数被赋值成null照样有意义的,我们经由历程剖析save函数前几行代码就能够很明显地剖析出缘由。

CollectionIterator

这是一个基于ES6的新的内容,目标是竖立一个遍历器,以后,我们能够在collection的一些要领中运用这个可遍历对象。

这个方面的学问能够看这里补充,一言半语也没法说清,简朴地讲,就是假如准确地定义了一个next属性要领,这个对象就能够根据本身定义的体式格局来遍历了。

而backbone这里定义的这个遍历器越发壮大,能够离别根据key、value、key和value三种体式格局遍历

我这里给出一个运用体式格局:

window.Test = Backbone.Model.extend({
    defaults: {content: ''
    }
});
// 竖立鸠合模子类  
window.TestList = Backbone.Collection.extend({
    model: Test
});
// 向模子增添数据
var data = new TestList(
        [
            {
                id:100,
                content: 'hello,backbone!'
            },
            {
                id:101,
                content: 'hello,Xiaotao!'
            }
        ]
);
for(var ii of data.keys()){
    console.log(ii);
}
for( ii of data.values()){
    console.log(ii);
}
for( ii of data.entries()){
    console.log(ii);
}

细致这里是怎样完成的,我置信人人看了上文链接给出的扩大学问以后,然后再连系我写了诠释的源代码,应当都能看懂了。

Collection其他内容

别的,Collection还完成了异常多的小要领,也混入了许多underscore的要领,但中心都是操纵this.modelsthis.models是一个平常的数组(所以,在js中本身完成了的要领也是能够在这里运用的),能够直接接见。

别的值得一提的是,Collection中有一个_byId变量,这个变量经由历程cid和id来存取,起到一个轻易直接存取的作用,在某些时刻异常轻易。

_addReference: function(model, options) {
      this._byId[model.cid] = model;
      var id = this.modelId(model.attributes);
      if (id != null) this._byId[id] = model;
      model.on('all', this._onModelEvent, this);
},

别的现实上,model除了作为Collection内里的元素,而且经由历程一个collection属性指向对应的Collection,现实上联络也并非异常多,这也比较相符低耦合高内聚的战略。

backbone View

接下来我们进入backbone的View部份,也就是和用户打交道的部份,我一最先用backbone的时刻就是被View层能够经由历程定义events对象数组来轻易地举行事宜治理所吸收(虽然现在看来另有更轻易的计划)

我们先来看一下View函数在用户新建View的时刻做了些什么:

 var View = Backbone.View = function(options) {
    this.cid = _.uniqueId('view');
    this.preinitialize.apply(this, arguments);
    //_.pick(object, *keys):返回一个object副本,只过滤出keys(有用的键构成的数组)参数指定的属性值。或许接收一个推断函数,指定遴选哪一个key。
    _.extend(this, _.pick(options, viewOptions));
    //初始化dom元素和jQuery元素事情
    this._ensureElement();
    //自定义初始化函数
    this.initialize.apply(this, arguments);
};

这内里值得一提的是this._ensureElement()这个函数,这个函数内部挪用了许多函数,做了许多事情,我们起首看这个函数:

_ensureElement: function() {
      if (!this.el) {
        var attrs = _.extend({}, _.result(this, 'attributes'));
        if (this.id) attrs.id = _.result(this, 'id');
        if (this.className) attrs['class'] = _.result(this, 'className');
        this.setElement(this._createElement(_.result(this, 'tagName')));
        this._setAttributes(attrs);
      } else {
        this.setElement(_.result(this, 'el'));
      }
},

根据你是不是传入一个dom元素(这个dom元素用来和View对应,也能够是jQuery元素)分成了两种状况实行,我们先看不传入的状况:

这个时刻我们能够定义一些属性,这些属性都在接下来赋值到天生的dom对象上:

 _setAttributes: function(attributes) {
      this.$el.attr(attributes);
}

接下来看假定传入了了的状况:

 setElement: function(element) {
      this.undelegateEvents();
      this._setElement(element);
      this.delegateEvents();
      return this;
},

这内里又挪用了三个函数,我们看一下这三个函数:

undelegateEvents: function() {
      if (this.$el) this.$el.off('.delegateEvents' + this.cid);
      return this;
},

_setElement: function(el) {
      this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
      this.el = this.$el[0];
},

delegateEvents: function(events) {
      events || (events = _.result(this, 'events'));
      if (!events) return this;
      this.undelegateEvents();
      for (var key in events) {
        var method = events[key];
        if (!_.isFunction(method)) method = this[method];
        if (!method) continue;
        var match = key.match(delegateEventSplitter);
        this.delegate(match[1], match[2], _.bind(method, this));
    }
    return this;
},

delegate: function(eventName, selector, listener) {
      this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
      return this;
},

上面第四个函数为第三个函数所挪用的,因而我们放在了一同。

第一个函数是解绑backbone所用的jQuery事宜定名空间下的事宜(.delegateEvents),这个是体式格局这个事宜被之前的其他View运用过,从而形成污染(现实上,这个平常状况下用的是不多的)。

第二个函数是初始化dom对象和jQuery对象,$el代表jQuery对象,el代表dom对象。

第三个函数是把我们写的监听事宜举行从新绑定,我们写的事宜满足下面的花样:

 //举个例子: 
 {
     'mousedown .title':  'edit',
     'click .button':     'save',
     'click .open':       function(e) { ... }
 }

上面第三个函数就是一个剖析函数,剖析好后直接挪用delegate函数举行事宜的绑定,这里要注重你定义的事宜的元素必需在供应的el内的,不然没法接见到。

render

别的,backbone中有一个render函数:

render: function() {
      return this;
},

这个render函数现实上有比较深远的意义,render函数默许是没有操纵的,我们能够本身定义操纵,然后能够在事宜中'change' 'render'如许对应,如许每次变化就会从新挪用render重绘,我们也能够自定义好render函数而且在初始化函数initialize中挪用。别的,render函数默许的return this;隐含了backbone的一种希冀:返回this从而支撑链式挪用。

render能够运用underscore的模版,而且这也是引荐做法,以下是一个异常简朴的demo:

var Bookmark = Backbone.View.extend({
  template: _.template(...),
  render: function() {
    this.$el.html(this.template(this.model.attributes));
    return this;
  }
});

backbone router、history

router

backbone比拟于一些盛行框架的优点就是本身完成了router部份,不必再引入其他插件,这点非常轻易。

我们在运用router的时刻,通常会采纳以下写法:

var Workspace = Backbone.Router.extend({

  routes: {
    "help":                 "help",    // #help
    "search/:query":        "search",  // #search/kiwis
    "search/:query/p:page": "search"   // #search/kiwis/p7
  },

  help: function() {
    ...
  },

  search: function(query, page) {
    ...
  }

});

router的供组织挪用的函数的主体部份也相称简朴,没有做过剩的事变:

var Router = Backbone.Router = function(options) {
    options || (options = {});
    this.preinitialize.apply(this, arguments);
    //注重这个处所,options的routes会直接this的routes,所以假如在竖立类的时刻指定routes,实例化的时刻又扩大了routes,是会被掩盖的
    if (options.routes) this.routes = options.routes;
    //对本身定义的路由举行处置惩罚
    this._bindRoutes();
    //挪用自定义初始化函数
    this.initialize.apply(this, arguments);
};

这里我们睁开_bindRoutes:

 _bindRoutes: function() {
      if (!this.routes) return;
      this.routes = _.result(this, 'routes');
      var route, routes = _.keys(this.routes);
      while ((route = routes.pop()) != null) {
        this.route(route, this.routes[route]);
      }
},

route函数是把路由处置惩罚成正则表达式情势,然后挪用history.route函数举行绑定,history.route函数在网址每次变化的时刻都邑搜检婚配,假如有婚配就实行回调函数,也就是下文Backbone.history.route传入的第二个参数,如许路由部份和history部份就联络在一同了。

route: function(route, name, callback) {
      //假如不是正则表达式,转换之
      if (!_.isRegExp(route)) route = this._routeToRegExp(route);
      if (_.isFunction(name)) {
        callback = name;
        name = '';
      }
      if (!callback) callback = this[name];
      var router = this;
      Backbone.history.route(route, function(fragment) {
        var args = router._extractParameters(route, fragment);
        if (router.execute(callback, args, name) !== false) {
          router.trigger.apply(router, ['route:' + name].concat(args));
          router.trigger('route', name, args);
          Backbone.history.trigger('route', router, name, args);
        }
      });
      return this;
},

上面的这段代码起首能够会挪用_routeToRegExp这个函数举行正则处置惩罚,这个函数多是backbone中最难明的函数,不过不懂也并不影响我们继承剖析(现实上,笔者也并没有完整懂这个函数,所以愿望履历人士能够在这里给予协助)。

 _routeToRegExp: function(route) {
      route = route.replace(escapeRegExp, '\\$&')//这个婚配的目标是将正则表达式字符举行转义
                   .replace(optionalParam, '(?:$1)?')
                   .replace(namedParam, function(match, optional) {
                     return optional ? match : '([^/?]+)';
                   })
                   .replace(splatParam, '([^?]*?)');
      return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
},

别的挪用了_extractParameters这个函数和router.execute这个函数,前者的作用就是将婚配胜利的URL中包含的参数转化成一个数组返回,后者接收三个参数,离别是回调函数,参数列表和函数名(这里之前只要两个函数,厥后backbone增添了第三个参数)。

 _extractParameters: function(route, fragment) {
      var params = route.exec(fragment).slice(1);
      return _.map(params, function(param, i) {
        // Don't decode the search params.
        if (i === params.length - 1) return param || null;
        return param ? decodeURIComponent(param) : null;
      });
}
execute: function(callback, args, name) {
      if (callback) callback.apply(this, args);
},

router的内容也就这些了,完成的比较简朴清新,代码也不多,关于处置惩罚汗青纪录浏览器兼容性的题目都放在了history部份,所以接下来我们来剖析难啃的history部份。

history

这一块的内容比较主要,而且比拟于之前的内容有些庞杂,我只管把本身的明白全都解说出来。

我们先申明一下这个汗青纪录的作用:
当你在浏览器接见的时刻,能够经由历程左上角的行进退却举行切换,这就是由于发生了汗青纪录。

那末什么体式格局能够发生汗青纪录呢?

  1. 页面跳转(一定的,然则并不适用于SPA)

  2. hash变化:形如<a href="#123"></a>这类点击后会触发汗青纪录),然则不幸的是在IE7下并不能被写入汗青纪录(虽然笔者是对IE9以下坚定说不的)

  3. pushState,这类比较牛逼,能够默默的转变路由,比方把article.html#article/54改成article.html#article/53然则不触发页面的革新,由于平常状况下这算是两个页面的,别的,这类状况须要服务端的支撑,因而我在用backbone的时刻较少采纳这类做法(现在有一个观点叫做pjax,就是ajax+pushState,细致能够Google之)

  4. iframe内url变化,变化iframe内的url也会触发汗青纪录,然则这个比较贫苦,别的,在IE中,不管iframe是一最先静态写在html中的照样厥后用js动态竖立的,都能够被写入浏览器的汗青纪录,其他浏览器平常只支撑静态写在html中。所以,我们平常在2&3都不可用的状况下,才选用这类状况(IE7以下)

以上讲的基础就是backbone运用的体式格局,接下来我们再根据backbone运用逻辑和优先级举行一些解说:

backbone默许是运用hash的,在不支撑hash的浏览器中运用iframe,假如想要运用pushState,须要显式声明而且浏览器本身要支撑(假如运用了pushState的话hash就不必了)。

所以backbone的history有一个异常大的start函数,这个函数从头至尾做了以下几件事变:

  • 将页面的根部份保留在root中,默许是/

  • 推断是不是想用hashChange(默许为true)以及支撑与否,推断是不是想用pushState以及支撑与否。

  • 推断一下究竟是用hash照样用push,而且做一些url处置惩罚

  • 假如须要用到iframe,这个时刻初始化一下iframe

  • 初始化监听事宜:用hash的话能够监听hashchange事宜,用pushState的话能够监听popState事宜,假如用了iframe,没办法,只能轮询了,这个主如果用来用户的行进退却。

  • 末了最主要的:先处置惩罚以下当前页面的路由,也就是说,假定用户直接接见的并非根页面,不能什么也不做呀,要挪用相干路由对应的函数,所以这里要挪用loadUrl

和start对应的stop函数,主要做了一些清算事情,假如能读懂start,那末stop函数应当是不难读懂的。

别的另有一个比较长的函数是navigate,这个函数的作用主如果存储/更新汗青纪录,主要和浏览器打交道,假如用hash的话,backbone本身是不会挪用这个函数的(由于用不到),然则能够供开辟者挪用:

开辟者能够经由历程这个函数用js代码自动治理路由:

openPage: function(pageNumber) {
  this.document.pages.at(pageNumber).open();
  this.navigate("page/" + pageNumber);
}

别的,backbone在这一部份定义了一系列东西函数,用于处置惩罚url。

backbone的history这一部份写的异常的优异,兼容性也异常的高,而且充足满足了高聚合低耦合的特征,假如本身也要完成history治理这一部份,那末backbone的这个history相对是一个优异的类型。

extend

末了,backbone还定义了一个extend函数,这个函数我们再熟习不过了,不过它的写法并没有我们设想的那末简朴,

这个函数并没有直接将属性assign到parent上面(this),是由于如许会发生一个明显的题目:污染原型
所以现实上backbone的做法是新建了一个子类,这个子对象承担著一切内容.

别的,这个extend函数也自创了ES6的一些写法,内容不多,明白起来也是简朴的。

ES6&backbone

backbone支撑ES6的写法,关于这个写法题目,曾GitHub上面有过猛烈的争辩,这里我稍作总结,先给出一个现在可行的写法:

class DocumentRow extends Backbone.View {

    preinitialize() {
        _.extend(this, {
          tagName:  "li",
          className: "document-row",
          events: {
            "click .icon":          "open",
            "click .button.edit":   "openEditDialog",
            "click .button.delete": "destroy"
          }
        });
    }

    initialize() {
        this.listenTo(this.model, "change", this.render);
    }

    render() {
        //...
    }
}

现实上,这个题目涌现之前backbone的源代码中是没有preinitialize函数的,关于为何终究是如许,我总结以下几点:

  • ES6的class不能直接写属性(直接报错),都要写成函数,由于假如有属性的话会涌现同享属性的题目。

  • ES6的class写法和ES5的不一样,也和backbone本身定义的extend是不一样的。是先要挪用父类的组织要领,然后再有子类的this,在挪用constructor之前是没法运用this的。所以下面这类写法就不行了:

class DocumentRow extends Backbone.View {

    constructor() {
        this.tagName =  "li";
        this.className = "document-row";
        this.events = {
            "click .icon":          "open",
            "click .button.edit":   "openEditDialog",
            "click .button.delete": "destroy"
        };
        super();
    }

    initialize() {
        this.listenTo(this.model, "change", this.render);
    }

    render() {
        //...
    }
}

然则假如把super提早,那末这个时刻tagName什么的还没有赋值呢,element就已竖立好了。

别的,把属性强迫写成函数的做法是被backbone支撑的,然则我置信没有多少人情愿如许做吧:

class DocumentRow extends Backbone.View {

    tagName() { return "li"; }

    className() { return "document-row";}

    events() {
        return {
            "click .icon":          "open",
            "click .button.edit":   "openEditDialog",
            "click .button.delete": "destroy"
        };
    }

    initialize() {
        this.listenTo(this.model, "change", this.render);
    }

    render() {
        //...
    }
}

所以我们须要:尽早把一些属性赋给父类掩盖掉父类默许属性,然后挪用父类组织函数,然后再挪用子类组织函数。所以到场一个preinitialize要领是一个比较好的遴选。

假如还没有明白,无妨看看下面这个实质等价的小例子:

class A{
    constructor(){
        this.s=1;
        this.preinit();
        this.dosomething();
        this.init();
    }
    preinit(){}
    init(){}
    dosomething(){console.log("dosomething:",this.s)}//dosomething 2
}
class B extends A{
    preinit(){this.s=2;}
    init(){}
}
var b1 = new B();
console.log(b1.s);//2

总结

经由以上冗长的对backbone源代码剖析的历程,我们了解了一个优异的框架的源代码,我总结了backbone源码的几个特征以下:

  • 充足发挥函数式编程的精力,相符函数式编程,之前有位先辈说对js的运用水平就取决于对js的函数式编程的熟悉水平,也是不无道理的。

  • 高内聚低耦合可扩大,这一方面轻易了我们运用backbone的一部份内容(比方只运用Events或许router),别的一方面也轻易了插件开辟,以及能和其他的库比较好的兼容,我以为,这并非一个强主意的库,你能够小规模地根据本身的体式格局运用,也能够大规模的完整根据backbone的希冀运用。

  • 在运用和兼容ES6的新特征上做了不少勤奋,在源代码中好几处都表现了ES6的内容,这让backbone作为一个老牌框架,在现在大规模运用做网页运用,依旧非常可行。

瑕玷:

  • backbone严峻依靠jQuery和underscore,这对backbone起到了管束作用,假定jQuery或许underscore转变了一个要领或许一个接口,那末backbone也要随着改,别的backbone依靠的jQuery和underscore也有一些限定,直接随意引入这三个文件极能够就会报错(平常状况下都引入最新的是没有题目的),这是backbone比较不好的一个处所(要不然本身也不能够做到这么轻量级)

参考资料
backbone官方文档:http://backbonejs.org/
backbone中文文档:http://www.css88.com/doc/back…
Why Backbone.js and ES6 Classes Don’t Mix:http://benmccormick.org/2015/…

关于backbone&ES6的议论:
https://github.com/jashkenas/…
https://github.com/jashkenas/…

    原文作者:aircloud
    原文地址: https://segmentfault.com/a/1190000008274936
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞