前端Backbone源码解读(二)

前端Backbone源码解读(二)

开场

强烈建议一边看着源码一边读本文章,本文不贴大段代码。源码地址

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

(最近要开学,要准备回学校上课,乱七八糟的东西很多,所以文章可能也会拖一阵子啦...但是我还是非常希望能够写下来,半途而废的感觉真心不好...

这一篇文章主要讲backbone的Model, Collection和View。这三个模块有很多相似的地方。这篇文章不会把模块的每一个方法都介绍一遍,因为只要看源码就知道,其实主要的方法只有几个,而很多其他的模块实际上只是在调用这几个核心的方法而已。

Model & Collection & View

首先讲一下三者的相似之处。这一节让我们来看看这三个模块一个总体结构。

这三个模块在结构上和Events不同。他们先通过以下方式来定义构造函数。(以View为例)

var View = Backbone.View = function(options) { 


    // 构造函数的内容 



};  

构造函数的内部一般会做以下几个操作:

  • 各种给内部对象设置属性。(各种this.a = b)
  • 调用preinitialize
this.preinitialize.apply(this, arguments); 
调用initialize
this.initialize.apply(this, arguments); 

各个模块的方法和属性是通过underscore的extend来获得的。注意在extend新加入的方法和属性中,以下划线开头的变量是内部函数名。(其实理论上用户也可以调用这些方法,谁叫Javascript没有内部变量呢...)这些内部方法是供自己模块内部调用的

_.extend(View.prototype, Events, { 


    // 这里是各种对View.prototype的拓展,定义各种方法 



});  

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

Model

关键方法

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

set

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

我想以变量的角度来讲解可能是一个比较好的角度。

  • changing和this._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也同理,因为不论中间如何变化,递归,到最后它会被设置为{}。

save

save方法的作用是把当前model的状态保存到数据库中,因此不可避免地要用到ajax。由于backbone已经有了一个封装好的方法sync用于触发ajax,因此在save当中重点是设置参数。需要设置的有success,error,method。

  • 在success里面会调用用户传入的回调函数并触发sync事件表示已经同步了。
  • error用封装好的wrapError函数,这个函数用得很多,用于处理错误。
  • method根据实际要用那种方法设置
    其中比较值得注意的是wait参数。这个参数会影响页面更新的时机。如果wait是true的话,就会需要等到服务器端相应才更新页面,否则就会立即更新。

destory

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

  • wait是false,不用等待。发起delete请求,触发内部函数destory。
  • wait是true,发起ajax,等待服务器响应才触发destory更新页面。
  • 这是一个新的model,那就不需要发起请求了。

isValid

验证函数,通过调用内部函数_validate,在通过这个函数调用validate函数。然后返回一个错误,如果没有错误就返回true,否则触发invalid,返回false。

Collection

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

set

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

  • 设置几个数组(下面会详细讲)
  • 设置实际的models(修改this.models)
  • trigger事件

主要来说就是有如下几个关键点:

  • 如果不符合model形式,转换之。
  • 设置相应的插入位置at。
  • 设置set数组。set数组在里面作用是为给后面排序做准备。里面存放的是新的Collection的models。
  • 设置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函数。而其他没有注册的函数应该是给使用者注册监听用的。

sort

sort所依据的是用户传入的comparator参数,这个参数可以是一个字符串也可以是一个函数,如果是字符串就通过underscore的sortBy方法,如果是个函数就直接传入sort的第二个参数中。

fetch & create

fetch和create是backbone与服务器端交互的一个接口。两个方法内部处理其实都很好理解,就是设置ajax参数。最终本质上都是触发sync。但是唯一不同的是fetch是通过自身的sync函数,但create是通过调用model的save,然后触发sync的。在

model.save(null, options); 

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

reference

在Collection有一个值得关注的内部变量,那就是_byId,这个变量用cid和id(所以model是一对一对出现的)来存储Collection里面的model,方便直接性的存取。在源码中有很多操作目的就是删除,增加,获取这个内部变量的值。

CollectionIterator

这东西我觉得很有意思...在官方文档里面没有提到,但是由于涉及到ES6的东西所以觉得有点眼前一亮的感觉(哈哈哈),backbone在这里用了Symbol.iterator,具体用法在这个链接里有介绍,还是挺清晰的。通过设置CollectionIterator的Symbol.iterator和next方法。它通过内部变量_kind来区分种类,_index来确定对应的next的结果,这个对于写迭代器还是有点借鉴意义的~

View

在写backbone应用的时候,View写着写着会越来越大...追根溯源,就是View的代码很少...(大雾)。关于View,在写相关代码的时候有一些值是需要设置的(可选的)。下面的代码就展示了可设置的参数,这些参数在View的方法中会用到(如果有的话)。

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

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

Element

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

Events

事件是View中非常重要的组成。这是用户可以操作数据的一个接口。在View里面和数据相关的方法有delegateEvents,delegates,undelegateEvents,undelegate。里面通过使用者设置的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; 



}  

因此在后续需要对整体的所有事件进行操作的时候就会方便很多很多。

最后的话

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

相关推荐