Backbone源码解读(二)

1. 开场

强烈建议一边看着源码一边读本文章,本文不贴大段代码。源码地址
在写backbone应用的时候,说实话,大部分的时间都是在写这三个模块的内容。关于这三个模块的分析网上随随便便就能找到一堆还不错的文章。但我但愿可以找到一条线索,能把各自模块的内部机理整理清楚。就像前一篇文章中介绍的Events那样。Events整个模块其实就是经过一些外部的方法来修改内部对象的属性,从而达到事件管理的目的。以一条线索来看待整个模块,一切都清晰了然了。下面就开始了~html

(最近要开学,要准备回学校上课,乱七八糟的东西不少,因此文章可能也会拖一阵子啦...可是我仍是很是但愿可以写下来,半途而废的感受真心很差...node

这一篇文章主要讲backboneModel, CollectionView。这三个模块有不少类似的地方。这篇文章不会把模块的每个方法都介绍一遍,由于只要看源码就知道,其实主要的方法只有几个,而不少其余的模块实际上只是在调用这几个核心的方法而已。jquery

2. Model & Collection & View

首先讲一下三者的类似之处。这一节让咱们来看看这三个模块一个整体结构。
这三个模块在结构上和Events不一样。他们先经过如下方式来定义构造函数。(以View为例)git

var View = Backbone.View = function(options) {
    // 构造函数的内容
};

构造函数的内部通常会作如下几个操做:github

  • 各类给内部对象设置属性。(各类this.a = bweb

  • 调用preinitializeajax

this.preinitialize.apply(this, arguments);
  • 调用initialize数据库

this.initialize.apply(this, arguments);

各个模块的方法和属性是经过underscoreextend来得到的。注意在extend新加入的方法和属性中,如下划线开头的变量是内部函数名。(其实理论上用户也能够调用这些方法,谁叫Javascript没有内部变量呢...)这些内部方法是供本身模块内部调用的。segmentfault

_.extend(View.prototype, Events, {
    // 这里是各类对View.prototype的拓展,定义各类方法
});

还有一个比较大的共同点,就是slient参数。这个参数决定了是否要trigger一个事件,在源码用占了很大的篇幅对其进行分类讨论。后端


3. Model

3.1 关键方法

有一些关键的方法一进入函数就会根据传入的参数的形态进行变化。由于backbone一些方法支持两个参数传入或者一个数组传入,这时候须要有个判断。

3.2 set

set方法在model里面是个很很差理解的东西,看了网上大多数解析感受都很模糊(并且遇到难理解的就用一些借口蒙混过去)。不得不说set里面复杂精妙程度是每读一遍惊叹一遍。
我想以变量的角度来说解多是一个比较好的角度。

  • changingthis._changing
    若是这个函数只是从头执行到尾,那说实话,这两个变量没有任何意义。由于他们的值是肯定的。看函数开头:

var changing   = this._changing;
this._changing = true;

在函数结尾:

this._changing = false;

这个changing将永远永远是false。我上网看到有人说多是webWorker,多线程相关的东西,但我直接在源码console的时候却发现,这个changing是会变的,并且我用得是todo范例。todo范例没有任何相似webWorker的东西。这个假设猜想应该来讲是不正确的。(不过这篇文章讲得也很不错啊)
因此这个changing到底有什么用呢?答案就是递归函数set里明明没有递归啊?其实递归藏在了全部trigger的事件的回调函数里面。源代码下面的这一段:

// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
while (this._pending) {
    options = this._pending;
    this._pending = false;
    this.trigger('change', this, options);
}

这一个while里的trigger使得函数发生递归,而后从新调用set。这样的话,下一次changing就等于true了,这个变量的做用才能发挥。能够看一下这个连接里面的讲解。

  • current变量是用来做为引用改变attributes的,实际上是set能设置attributes的本质。

  • changes数组是用来存放改变了的key的,用于后期的事件触发。

  • changed & _previousAttributes
    把这两个放到一块儿是由于他们的一个特殊的地方。我在todo的主函数的render里面console,发现不论我作什么操做,changed === {},_previousAttributes没有发生改变。后来在查看官方文档的时候,才了解previous的用法:

var bill = new Backbone.Model({
  name: "Bill Smith"
});

bill.on("change:name", function(model, name) {
  alert("Changed name from " + bill.previous("name") + " to " + name);
});

bill.set({name : "Bill Jones"});

set方法在被调用的时候,previous只有在回调函数里才能有用,也就是说,在回调函数外面想要用这个previous获取前一个值是不可能的。它只能获取到当前值。为何呢?源码作出了解释。当用户作出操做须要用到set方法的时候,其实set方法并非直接执行完就结束了。在这个方法里面触发了不少的事件,而previous只有在函数里触发了的事件的回调函数“里面”才能返回正确的“前一个值”。changed也同理,由于不论中间如何变化,递归,到最后它会被设置为{}

3.3 save

save方法的做用是把当前model的状态保存到数据库中,所以不可避免地要用到ajax。因为backbone已经有了一个封装好的方法sync用于触发ajax,所以在save当中重点是设置参数。须要设置的有successerrormethod

  • success里面会调用用户传入的回调函数并触发sync事件表示已经同步了。

  • error用封装好的wrapError函数,这个函数用得不少,用于处理错误。

  • method根据实际要用那种方法设置
    其中比较值得注意的是wait参数。这个参数会影响页面更新的时机。若是waittrue的话,就会须要等到服务器端相应才更新页面,不然就会当即更新。

3.4 destory

destory方法也是与ajax有密切联系的。主要也是设置ajax参数。它分了几种不一样的状况并做出了相应的处理:

  • waitfalse,不用等待。发起delete请求,触发内部函数destory

  • waittrue,发起ajax,等待服务器响应才触发destory更新页面。

  • 这是一个新的model,那就不须要发起请求了。

3.5 isValid

验证函数,经过调用内部函数_validate,在经过这个函数调用validate函数。而后返回一个错误,若是没有错误就返回true,不然触发invalid,返回false


4. Collection

Collection相似一个数组,里面存放着各类以model为结构的对象。在Collection中也有这形式的判断,若是传入的参数是单个对象就会被转换成数组。

4.1 set

这是Collection的一个很经常使用的方法,源码中这一段很长,也有点繁琐,可是没有特别难以理解的地方。整个set的结构是:

  • 设置几个数组(下面会详细讲)

  • 设置实际的models(修改this.models

  • trigger事件

主要来讲就是有以下几个关键点:

  • 若是不符合model形式,转换之。

  • 设置相应的插入位置at

  • 设置set数组。set数组在里面做用是为给后面排序作准备。里面存放的是新的Collectionmodels

  • 设置toAdd数组。这个数组是用于存储新建的合法的model,而后须要调用内部函数_addReference设置索引于_byId数组,而且添加all事件(后面就能够经过model直接trigger事件)。当slient不是true,后期能够经过遍历它来触发add事件。

  • 设置toMerge数组。当这个model是本来已经存在的model的时候(cid匹配),就会修改,而后被push进这个数组中。

  • 设置toRemove数组。而后经过内部函数_removeModels删除那些已经不在set里面的models

  • 修改this.models,分两种状况,一种是直接整个替换掉,一种是后面再添加。

  • 若是silent不是true就要触发事件。特别值得注意的一点是:这里面的事件有两种,一种事件是由Model发出的,一种事件是有Collection发出的。从Model发出的事件能够很容易_addReference函数中发现

model.on('all', this._onModelEvent, this);

在这里注册了,调用的是_onModelEvent函数。而其余没有注册的函数应该是给使用者注册监听用的。

4.2 sort

sort所依据的是用户传入的comparator参数,这个参数能够是一个字符串也能够是一个函数,若是是字符串就经过underscoresortBy方法,若是是个函数就直接传入sort的第二个参数中。

4.3 fetch & create

fetchcreatebackbone与服务器端交互的一个接口。两个方法内部处理其实都很好理解,就是设置ajax参数。最终本质上都是触发sync。可是惟一不一样的是fetch是经过自身的sync函数,但create是经过调用modelsave,而后触发sync的。在

model.save(null, options);

跟着这个save函数里面走,就会发现参数null传入是有意义的。在save里面的参数设置会很好地赋值并最后触发sync,并且有一个颇有趣的点,就是这个createmodel传上服务器,可是这个model是一个相对独立的状态,仅仅经过它的Collection属性来维系和Collection的关系。那就要求后端须要把这一个model添加到相应的Collection数据里面去。

4.4 reference

Collection有一个值得关注的内部变量,那就是_byId,这个变量用cidid(因此model是一对一对出现的)来存储Collection里面的model,方便直接性的存取。在源码中有不少操做目的就是删除,增长,获取这个内部变量的值。

4.5 CollectionIterator

这东西我以为颇有意思...在官方文档里面没有提到,可是因为涉及到ES6的东西因此以为有点眼前一亮的感受(哈哈哈),backbone在这里用了Symbol.iterator,具体用法在这个连接里有介绍,仍是挺清晰的。经过设置CollectionIteratorSymbol.iteratornext方法。它经过内部变量_kind来区分种类,_index来肯定对应的next的结果,这个对于写迭代器仍是有点借鉴意义的~


5. View

在写backbone应用的时候,View写着写着会愈来愈大...追根溯源,就是View的代码不多...(大雾)。关于View,在写相关代码的时候有一些值是须要设置的(可选的)。下面的代码就展现了可设置的参数,这些参数在View的方法中会用到(若是有的话)。

var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];

下面我会从两个大的方面来解读源码,一个是element,一个是Events。整个View的源码事实上就是这两组东西。

5.1 Element

View字面意思是视图,而在浏览器中,视图就是html所呈现的页面。每个View事实上就对应着html的一个元素(固然这个元素里面能够有不少不少元素)。这个元素默认标签是div。与元素相关的代码其实很简单,首先要认清this.elthis.$el。前者是真正的节点,后者则是jquery对象的节点。后者因为是jquery式的,所以就能够作相关的jquery的操做。所以事件发起,删除节点,设置属性的操做都是jqueryapithis.$el或其子节点的操做。在进入构造函数的时候会调用一个叫_ensureElement的内部函数,在这个函数里会根据用户设置的参数去构建节点,最后展示到页面之上。

5.2 Events

事件是View中很是重要的组成。这是用户能够操做数据的一个接口。在View里面和数据相关的方法有delegateEventsdelegatesundelegateEventsundelegate。里面经过使用者设置的events属性来建立各类事件,操做各类事件。

{
    'mousedown .title':  'edit',
    'click .button':     'save',
    'click .open':       function(e) { ... }
}

events相关代码很简单,可是有一个很是很是巧妙的地方:就是做者用了jquery事件相关api命名空间。在delegate被调用的时候就给事件加上了一个特定的命名空间。

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

所以在后续须要对总体的全部事件进行操做的时候就会方便不少不少。


6. 最后的话

此次源码解析不能百分百保证是正确的,有一些混杂了本身的思考。由于不想像其余大部分的源码解析那样,对于问题模糊处理。但我以为仍是有意义的,由于每一个人读的角度不同。兼听则明,也但愿读者可以包容,但愿深入理解backbone的读者也请多读几篇文章,多读几遍源码。下一篇文章要写router & history,这一个模块能够单独拆出来做为SPA的一个入口,我的认为这部分时backbonebackbone(骨架)。

但愿可以坚持更下去吧,开学了,事情也开始多了起来...

本人仍是backbone小白,若是哪里说错了或者怎样,请轻喷~相互学习~

下面是所有的文章:

相关文章
相关标签/搜索