Que's Blog

WebFrontEnd Development

0%

backbone初探(三)-整体结构

Structure

Backbone的整体Structure

webStorm的Structure是个很不错的功能。只是很多源码确实不是仅仅看这个就可以有相对清晰的脉络。但是这里似乎并不包括Backbone。

很清晰的结构,而且最重要的几个工具函数在上一篇已经有了相对深入的解读。
所以接下来我们对Backbone的若干个构造器来个相对粗略的了解。

Event结构

Event是个很重要的部分,不管是Model,Collection还是View,均在原型上继承了它的各种方法。
但是Event如果不论细节,它的实现原理还是非常简单的。
如果上图所示,Event有两个基础方法:evnetApi和trigerApi,其中eventApi在AOP编程思想基础上,构建了4个方法,internalOn,onApi,offApi和onceApi,这里internalOn是队opApi的进一步封装用于设置_listeners和_event以便进行跟踪,本篇主要侧重于结构分析,细节将下一篇开始分解。然后在看trigerApi,在它基础上还有一个triggerEvents。

在这两个基础方法基础上,Event封装如果暴露出来的方法。
eventApi:

  • on
  • off
  • once
  • listenToOnce
  • stopListening
  • ListenTo

onceApi

  • trigger

Model结构

首先要声明一点,Backbone.Model.extend()返回的是一个构造器。extend整个细节在上一篇已经深入讲过了。child.prototype.constructor被更改返回child以后,整个新的构造器已经被切断了和Backbone.Model的关联。Backbone.Model其实已经可以视为一个生产构造器工厂。

不独是Model,其他的View、Collection其实也是一样。extend使用了闭包将内部变量返回,当Backbone.Model.extend()执行完毕之后,返回值其实同Backbone.Model没有任何关联(除了特别设置的_super_)

这里看看Model生成新的构造器时候干了什么

回顾extend一段代码:

1
child = function(){ return parent.apply(this, arguments); };

这里this指向的是Model,Collection和View之类,以Model为例,它实质上将构造函数copy一个赋值给child了而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Todo = Backbone.Model.extend({
// Default attributes for the todo item.
defaults: function() {
return {
title: "empty todo...",
done: false
};
},

// Toggle the `done` state of this todo item.
toggle: function() {
this.save({done: !this.get("done")});
}

});
var test = new Todo();

console.log(test)的结果是:

这里attributes是存放数据的对象,代表的是,一个实例的数据。但是,toggle在原型上了,似乎忽略了一些细节。
回看extend代码,是这里运行导致的:

1
child.prototype = _.create(parent.prototype, protoProps);

Collection结构


实际上Collection同Model的构造环节几乎一模一样,只是构造函数有所不同。
看看构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  var Collection = Backbone.Collection = function(models, options) {
//options如果没有传入将初始化为一个空对象
options || (options = {});
//options.model若指定则设置实例的model属性为该值
if (options.model) this.model = options.model;
//options.comparator若指定则设置实例的comparator属性为该值
if (options.comparator !== void 0) this.comparator = options.comparator;
//初始化实例的length,models和_byId属性
this._reset();
//this.initialize(arguments) 使用initialize来初始化实例
this.initialize.apply(this, arguments);
// 如果models存在
if (models) this.reset(models, _.extend({silent: true}, options));
};

很明晰的构造函数,大致是分成三步

  1. 如果options传入且存在model或者comparator,那么将他们挂到实例属性上,否则设置为空对象。
  2. 然后使用先后使用_reset()和initialize来初始化实例属性。
  3. 最后一步,如果models传入了,那么调用reset来将models加入到实例的内部数据属性上。

这里稍微有点问题的是第二步和第三步:

_reset()

_reset是原型上自带的一个初始化函数,而initialize默认是空函数,但是可以通过extend中以下代码来设置一个,也就是说是可以配置的。

1
child.prototype = _.create(parent.prototype, protoProps);

reset

reset是另外一个初始化函数,这里贴一下细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// When you have more items than you want to add or remove individually,
// 当你有更多数据要逐个添加或者删除
// you can reset the entire set with a new list of models, without firing
// 你可以重置这个实例设置一个新的models列表,不用触发任何add和remove的事件
// any granular `add` or `remove` events. Fires `reset` when finished.
// 添加删除完毕后触发reset事件
// Useful for bulk operations and optimizations.
// 对批量操作和优化来说非常有用
reset: function(models, options) {
//options存在则深拷贝一份,或设置为{}
options = options ? _.clone(options) : {};
//遍历models,切断模型和某个集合的关系。
for (var i = 0; i < this.models.length; i++) {
this._removeReference(this.models[i], options);
}
//将options的previousModels指向这个models
options.previousModels = this.models;
//初始化实例的length,models和_byId属性 ->此时this.models=[];
this._reset();
//调用add方法添加models到Collection
models = this.add(models, _.extend({silent: true}, options));
//如果options设置了silent=false,手动触发reset事件进行重置
if (!options.silent) this.trigger('reset', this, options);
return models;
},

代码已经注释了每一步的逻辑,这里有个需要说的是reset方法本身并不处理数据的增删,而是依赖add方法,add又依赖set方法,基本上set方法可以说是Collection最基础的方法之一了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
add: function(models, options) {
return this.set(models, _.extend({merge: false}, options, addOptions));
}
...
// Update a collection by `set`-ing a new list of models, adding new ones,
// removing models that are no longer present, and merging models that
// already exist in the collection, as necessary. Similar to **Model#set**,
// the core operation for updating the data contained by the collection.
set: function(models, options) {
//models为空直接返回false跳出函数
if (models == null) return;

// 合并setOptions和options赋值给options
options = _.extend({}, setOptions, options);

// 分支1:如果options设置了parse且models不是一个Model实例,那么使用parse方法来解析一下models
if (options.parse && !this._isModel(models)) {
models = this.parse(models, options) || [];
}

// 分支2:判断一下models是否是数组,如果是数组,返回一个副本,
// 否则放到一个空素组作为元素并返回数组->统一转换成数组处理
var singular = !_.isArray(models);
models = singular ? [models] : models.slice();

// 设置at 如果指定了at,那么排序就无意义
// 如果at不为空强制转为数字(正常范围内)
// 如果at大于数组长度: 设为数组长度
// 如果at小余0:反向设置索引
var at = options.at;
if (at != null) at = +at;
if (at > this.length) at = this.length;
if (at < 0) at += this.length + 1;

// 准备要放数据的设置,添加,合并,移除和modelMap的空数组
var set = [];
var toAdd = [];
var toMerge = [];
var toRemove = [];
var modelMap = {};

// 为各个数据设置引用
var add = options.add;
var merge = options.merge;
var remove = options.remove;

// 设置排序状态,是否可以排序和排序属性
var sort = false;
var sortable = this.comparator && at == null && options.sort !== false;
var sortAttr = _.isString(this.comparator) ? this.comparator : null;

// Turn bare objects into model references, and prevent invalid models
// from being added.
// 将纯对象变成model引用,防止无效的models被加进来。
var model, i;
for (i = 0; i < models.length; i++) {
model = models[i];

// If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model.
// 如果一个副本已存在,防止被加进来,可选是否合并它到已存在的model
var existing = this.get(model); //获取已经存在的model数组
if (existing) {
// 如果存在,判断options.merge是否存在&&option的merge是否没有加入过
// 如果是 处理数据压入到toMerge
if (merge && model !== existing) {
var attrs = this._isModel(model) ? model.attributes : model;
if (options.parse) attrs = existing.parse(attrs, options);
existing.set(attrs, options);
toMerge.push(existing);
if (sortable && !sort) sort = existing.hasChanged(sortAttr);
}
// 如果存在且modelMap中没有这个cid
// 那么设置modelMap对应的cid属性值为true,压入existing到set数组
if (!modelMap[existing.cid]) {
modelMap[existing.cid] = true;
set.push(existing);
}

// models[i]设为处理后的existing
models[i] = existing;

// If this is a new, valid model, push it to the `toAdd` list.
// 如果是一个新的有效的model,将它压入到toAdd数组
} else if (add) {
model = models[i] = this._prepareModel(model, options);
if (model) {
toAdd.push(model);
this._addReference(model, options);
modelMap[model.cid] = true;
set.push(model);
}
}
}

// Remove stale models.
// 移除过期的models
if (remove) {
for (i = 0; i < this.length; i++) {
model = this.models[i];
if (!modelMap[model.cid]) toRemove.push(model);
}
if (toRemove.length) this._removeModels(toRemove, options);
}

// See if sorting is needed, update `length` and splice in new models.
// 判断是否需要排序,更新length并切片到新的models列表
var orderChanged = false;
var replace = !sortable && add && remove;
if (set.length && replace) {
orderChanged = this.length !== set.length || _.some(this.models, function(m, index) {
return m !== set[index];
});
this.models.length = 0;
splice(this.models, set, 0);
this.length = this.models.length;
} else if (toAdd.length) {
if (sortable) sort = true;
//如果at存在插入到at后面 不然插入到最后
splice(this.models, toAdd, at == null ? this.length : at);
this.length = this.models.length;
}

// Silently sort the collection if appropriate.
// 静默排序
if (sort) this.sort({silent: true});

// Unless silenced, it's time to fire all appropriate add/sort/update events.
// 除非设置了sliencd,是时候去触发所有的添加删除排序更新操作
if (!options.silent) {
for (i = 0; i < toAdd.length; i++) {
if (at != null) options.index = at + i;
model = toAdd[i];
model.trigger('add', model, this, options);
}
if (sort || orderChanged) this.trigger('sort', this, options);
if (toAdd.length || toRemove.length || toMerge.length) {
options.changes = {
added: toAdd,
removed: toRemove,
merged: toMerge
};
this.trigger('update', this, options);
}
}

// Return the added (or merged) model (or models).
return singular ? models[0] : models;
},

set方法实在是个很重量级的方法了,逻辑很长,考虑到的东西非常多。
我们重点说说这个set方法,他的用途是什么呢?我们先翻译一遍这个注释

更新一个集合,通过set一个新的集合列表。添加一个新集合,或者删除不再存在的集合,
同时,合并已经存在的集合——如果需要的话。类似Model的set方法,是collection更新数据的
核心方法。

set方法如果全部记住的话实在太复杂了,一个函数长度达到一百行实际项目中也见到的不多。还是那句,先宏观到微观,步步为营深入它。

  1. 准备后面函数需要用到的数据:
  • line11-23:设置options,然后设置model:

– parse存在且不为模型时,用parse处理model;
– 判断是否是数组,如果是数组返回副本,否则放到一个空素组作为元素并返回数组

  • line26-50:设置at索引,临时容器数组,和排序属性(状态,能力,属性)
  1. 数据操作流程:
  • 将纯对象变成model引用,方式是遍历数组,取出每个属性到以下流程

– 判断是否已有这个数据
— 是->如果设置了合并相关选项,那么进行相关事宜
— 否->进入添加流程

  • 移除过期的models
  • 更新lenght&排序
  • 触发绑定的事件

这样就够了。里面更多细节我认为已经不需要去深究了。阅读代码更多是在读架构。这里如果没有以后发现有补充必要,暂时先跳过了。实际上注释已经加的不少了。

View

view的结构和上面都一样,但是相比上列,它的构造函数实在简洁的不行。

1
2
3
4
5
6
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
_.extend(this, _.pick(options, viewOptions));
this._ensureElement();
this.initialize.apply(this, arguments);
};

加起来一共也就4行,第一行设置cid;第二行从options中过滤出[‘model’, ‘collectgition’, ‘el’, ‘id’, ‘attributes’, ‘className’, ‘tagName’, ‘events’]数组中的属性;第三行用到了_ensureElement方法,用来确保获取dom(class,id或者选择器字符串),同时这个函数里面还对事件进行了绑定;第四行用来初始化,默认是个空函数,可以自定义。

除此以外View几乎没有其他的API被暴露出来。View的主要的API只有渲染、绑定事件和移除3种。但是简单不代表View不重要,后面文章再深入尝试使用。

History&&Route

说实话History这个模块确实没有想到,可能是因为过去项目尚没有这方面的苛刻要求。
至于在html5的pushState和hashchange事件的基础上进行简单前进后退的实践却确实有不少,但是Histroy这个环节里面的处理方式确实让我始料未及。

这里随便列举一下里面包含的没有想过的:

  • getSearch:在更多时候个人往往使用location.query来获取这个,但是没想过ie6会存在bug,这里使用正则获取
  • getHash:个人用的location.hash来获取,没遇到过firefox这个bug过
  • decodeFragment解码location.pathname这个%25的例外也不知道

至于细节暂时搁置一下。毕竟Backbone我更想看看MVC三个模块之间的数据是怎样的。收拾一下,准备开始实例看数据。

就这样吧。此篇暂结。