1. 开场
强烈建议一边看着源码一边读本文章,本文不贴大段代码。源码地址。
在写backbone
应用的时候,说实话,大部分的时间都是在写这三个模块的内容。关于这三个模块的分析网上随随便便就能找到一堆还不错的文章。但我希望能够找到一条线索,能把各自模块的内部机理整理清楚。就像前一篇文章中介绍的Events
那样。Events
整个模块其实就是通过一些外部的方法来修改内部对象的属性,从而达到事件管理的目的。以一条线索来看待整个模块,一切都清晰了然了。下面就开始了~
(最近要开学,要准备回学校上课,乱七八糟的东西很多,所以文章可能也会拖一阵子啦...但是我还是非常希望能够写下来,半途而废的感觉真心不好...
这一篇文章主要讲backbone
的Model
, Collection
和View
。这三个模块有很多相似的地方。这篇文章不会把模块的每一个方法都介绍一遍,因为只要看源码就知道,其实主要的方法只有几个,而很多其他的模块实际上只是在调用这几个核心的方法而已。
2. 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
一个事件,在源码用占了很大的篇幅对其进行分类讨论。
3. Model
3.1 关键方法
有一些关键的方法一进入函数就会根据传入的参数的形态进行变化。因为backbone一些方法支持两个参数传入或者一个数组传入,这时候需要有个判断。
3.2 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
也同理,因为不论中间如何变化,递归,到最后它会被设置为{}
。
3.3 save
save
方法的作用是把当前model
的状态保存到数据库中,因此不可避免地要用到ajax
。由于backbon
e已经有了一个封装好的方法sync
用于触发ajax
,因此在save
当中重点是设置参数。需要设置的有success
,error
,method
。
在
success
里面会调用用户传入的回调函数并触发sync
事件表示已经同步了。error
用封装好的wrapError
函数,这个函数用得很多,用于处理错误。method
根据实际要用那种方法设置
其中比较值得注意的是wait
参数。这个参数会影响页面更新的时机。如果wait
是true
的话,就会需要等到服务器端相应才更新页面,否则就会立即更新。
3.4 destory
destory
方法也是与ajax
有密切联系的。主要也是设置ajax
参数。它分了几种不同的情况并作出了相应的处理:
wait
是false
,不用等待。发起delete
请求,触发内部函数destory
。wait
是true
,发起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
数组在里面作用是为给后面排序做准备。里面存放的是新的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
函数。而其他没有注册的函数应该是给使用者注册监听用的。
4.2 sort
sort
所依据的是用户传入的comparator
参数,这个参数可以是一个字符串也可以是一个函数,如果是字符串就通过underscore
的sortBy
方法,如果是个函数就直接传入sort
的第二个参数中。
4.3 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
数据里面去。
4.4 reference
在Collection
有一个值得关注的内部变量,那就是_byId
,这个变量用cid
和id
(所以model
是一对一对出现的)来存储Collection
里面的model
,方便直接性的存取。在源码中有很多操作目的就是删除,增加,获取这个内部变量的值。
4.5 CollectionIterator
这东西我觉得很有意思...在官方文档里面没有提到,但是由于涉及到ES6
的东西所以觉得有点眼前一亮的感觉(哈哈哈),backbone
在这里用了Symbol.iterator
,具体用法在这个链接里有介绍,还是挺清晰的。通过设置CollectionIterator
的Symbol.iterator
和next
方法。它通过内部变量_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.el
和this.$el
。前者是真正的节点,后者则是jquery
对象的节点。后者由于是jquery
式的,因此就可以做相关的jquery
的操作。因此事件发起,删除节点,设置属性的操作都是jquery
的api
对this.$el
或其子节点的操作。在进入构造函数的时候会调用一个叫_ensureElement
的内部函数,在这个函数里会根据用户设置的参数去构建节点,最后展现到页面之上。
5.2 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;
}
因此在后续需要对整体的所有事件进行操作的时候就会方便很多很多。
6. 最后的话
这次源码解析不能百分百保证是正确的,有一些混杂了自己的思考。因为不想像其他大部分的源码解析那样,对于问题模糊处理。但我觉得还是有意义的,因为每个人读的角度不一样。兼听则明,也希望读者能够包容,希望深刻理解backbone
的读者也请多读几篇文章,多读几遍源码。下一篇文章要写router & history
,这一个模块可以单独拆出来作为SPA
的一个入口,个人认为这部分时backbone
的backbone
(骨架)。
希望能够坚持更下去吧,开学了,事情也开始多了起来...
本人还是backbone
小白,如果哪里说错了或者怎样,请轻喷~相互学习~
下面是全部的文章:
基于 Backbone + node 的个人简历生成器(个人学习总结)
Backbone源码解读(一)
Backbone源码解读(二)
Backbone源码解读(三)