forked from YuanHuCoding/SourceAnalysis_BackboneJS
-
Notifications
You must be signed in to change notification settings - Fork 0
/
backbone-0.9.2源码解析.js
1986 lines (1878 loc) · 120 KB
/
backbone-0.9.2源码解析.js
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Backbone.js 0.9.2
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
(function() {
// 创建一个全局对象, 在浏览器中表示为window对象, 在Node.js中表示global对象
var root = this;
// 保存"Backbone"变量被覆盖之前的值
// 如果出现命名冲突或考虑到规范, 可通过Backbone.noConflict()方法恢复该变量被Backbone占用之前的值, 并返回Backbone对象以便重新命名
var previousBackbone = root.Backbone;
// 将Array.prototype中的slice和splice方法缓存到局部变量以供调用
var slice = Array.prototype.slice;
var splice = Array.prototype.splice;
var Backbone;
if( typeof exports !== 'undefined') {
Backbone = exports;
} else {
Backbone = root.Backbone = {};
}
// 定义Backbone版本
Backbone.VERSION = '0.9.2';
// 在服务器环境下自动导入Underscore, 在Backbone中部分方法依赖或继承自Underscore
var _ = root._;
if(!_ && ( typeof require !== 'undefined'))
_ = require('underscore');
// 定义第三方库为统一的变量"$", 用于在视图(View), 事件处理和与服务器数据同步(sync)时调用库中的方法
// 支持的库包括jQuery, Zepto等, 它们语法相同, 但Zepto更适用移动开发, 它主要针对Webkit内核浏览器
// 也可以通过自定义一个与jQuery语法相似的自定义库, 供Backbone使用(有时我们可能需要一个比jQuery, Zepto更轻巧的自定义版本)
// 这里定义的"$"是局部变量, 因此不会影响在Backbone框架之外第三方库的正常使用
var $ = root.jQuery || root.Zepto || root.ender;
// 手动设置第三方库
// 如果在导入了Backbone之前并没有导入第三方库, 可以通过setDomLibrary方法设置"$"局部变量
// setDomLibrary方法也常用于在Backbone中动态导入自定义库
Backbone.setDomLibrary = function(lib) {
$ = lib;
};
// 放弃以"Backbone"命名框架, 并返回Backbone对象, 一般用于避免命名冲突或规范命名方式
// 例如:
// var bk = Backbone.noConflict(); // 取消"Backbone"命名, 并将Backbone对象存放于bk变量中
// console.log(Backbone); // 该变量已经无法再访问Backbone对象, 而恢复为Backbone定义前的值
// var MyBackbone = bk; // 而bk存储了Backbone对象, 我们将它重命名为MyBackbone
Backbone.noConflict = function() {
root.Backbone = previousBackbone;
return this;
};
// 对于不支持REST方式的浏览器, 可以设置Backbone.emulateHTTP = true
// 与服务器请求将以POST方式发送, 并在数据中加入_method参数标识操作名称, 同时也将发送X-HTTP-Method-Override头信息
Backbone.emulateHTTP = false;
// 对于不支持application/json编码的浏览器, 可以设置Backbone.emulateJSON = true;
// 将请求类型设置为application/x-www-form-urlencoded, 并将数据放置在model参数中实现兼容
Backbone.emulateJSON = false;
// Backbone.Events 自定义事件相关
// -----------------
// eventSplitter指定处理多个事件时, 事件名称的解析规则
var eventSplitter = /\s+/;
// 自定义事件管理器
// 通过在对象中绑定Events相关方法, 允许向对象添加, 删除和触发自定义事件
var Events = Backbone.Events = {
// 将自定义事件(events)和回调函数(callback)绑定到当前对象
// 回调函数中的上下文对象为指定的context, 如果没有设置context则上下文对象默认为当前绑定事件的对象
// 该方法类似与DOM Level2中的addEventListener方法
// events允许指定多个事件名称, 通过空白字符进行分隔(如空格, 制表符等)
// 当事件名称为"all"时, 在调用trigger方法触发任何事件时, 均会调用"all"事件中绑定的所有回调函数
on : function(events, callback, context) {
// 定义一些函数中使用到的局部变量
var calls, event, node, tail, list;
// 必须设置callback回调函数
if(!callback)
return this;
// 通过eventSplitter对事件名称进行解析, 使用split将多个事件名拆分为一个数组
// 一般使用空白字符指定多个事件名称
events = events.split(eventSplitter);
// calls记录了当前对象中已绑定的事件与回调函数列表
calls = this._callbacks || (this._callbacks = {});
// 循环事件名列表, 从头至尾依次将事件名存放至event变量
while( event = events.shift()) {
// 获取已经绑定event事件的回调函数
// list存储单个事件名中绑定的callback回调函数列表
// 函数列表并没有通过数组方式存储, 而是通过多个对象的next属性进行依次关联
/** 数据格式如:
* {
* tail: {Object},
* next: {
* callback: {Function},
* context: {Object},
* next: {
* callback: {Function},
* context: {Object},
* next: {Object}
* }
* }
* }
*/
// 列表每一层next对象存储了一次回调事件相关信息(函数体, 上下文和下一次回调事件)
// 事件列表最顶层存储了一个tail对象, 它存储了最后一次绑定回调事件的标识(与最后一次回调事件的next指向同一个对象)
// 通过tail标识, 可以在遍历回调列表时得知已经到达最后一个回调函数
list = calls[event];
// node变量用于记录本次回调函数的相关信息
// tail只存储最后一次绑定回调函数的标识
// 因此如果之前已经绑定过回调函数, 则将之前的tail指定给node作为一个对象使用, 然后创建一个新的对象标识给tail
// 这里之所以要将本次回调事件添加到上一次回调的tail对象, 是为了让回调函数列表的对象层次关系按照绑定顺序排列(最新绑定的事件将被放到最底层)
node = list ? list.tail : {};
node.next = tail = {};
// 记录本次回调的函数体及上下文信息
node.context = context;
node.callback = callback;
// 重新组装当前事件的回调列表, 列表中已经加入了本次回调事件
calls[event] = {
tail : tail,
next : list ? list.next : node
};
}
// 返回当前对象, 方便进行方法链调用
return this;
},
// 移除对象中已绑定的事件或回调函数, 可以通过events, callback和context对需要删除的事件或回调函数进行过滤
// - 如果context为空, 则移除所有的callback指定的函数
// - 如果callback为空, 则移除事件中所有的回调函数
// - 如果events为空, 但指定了callback或context, 则移除callback或context指定的回调函数(不区分事件名称)
// - 如果没有传递任何参数, 则移除对象中绑定的所有事件和回调函数
off : function(events, callback, context) {
var event, calls, node, tail, cb, ctx;
// No events, or removing *all* events.
// 当前对象没有绑定任何事件
if(!( calls = this._callbacks))
return;
// 如果没有指定任何参数, 则移除所有事件和回调函数(删除_callbacks属性)
if(!(events || callback || context)) {
delete this._callbacks;
return this;
}
// 解析需要移除的事件列表
// - 如果指定了events, 则按照eventSplitter对事件名进行解析
// - 如果没有指定events, 则解析已绑定所有事件的名称列表
events = events ? events.split(eventSplitter) : _.keys(calls);
// 循环事件名列表
while( event = events.shift()) {
// 将当前事件对象从列表中移除, 并缓存到node变量中
node = calls[event];
delete calls[event];
// 如果不存在当前事件对象(或没有指定移除过滤条件, 则认为将移除当前事件及所有回调函数), 则终止此次操作(事件对象在上一步已经移除)
if(!node || !(callback || context))
continue;
// Create a new list, omitting the indicated callbacks.
// 根据回调函数或上下文过滤条件, 组装一个新的事件对象并重新绑定
tail = node.tail;
// 遍历事件中的所有回调对象
while(( node = node.next) !== tail) {
cb = node.callback;
ctx = node.context;
// 根据参数中的回调函数和上下文, 对回调函数进行过滤, 将不符合过滤条件的回调函数重新绑定到事件中(因为事件中的所有回调函数在上面已经被移除)
if((callback && cb !== callback) || (context && ctx !== context)) {
this.on(event, cb, ctx);
}
}
}
return this;
},
// 触发已经定义的一个或多个事件, 依次执行绑定的回调函数列表
trigger : function(events) {
var event, node, calls, tail, args, all, rest;
// 当前对象没有绑定任何事件
if(!( calls = this._callbacks))
return this;
// 获取回调函数列表中绑定的"all"事件列表
all = calls.all;
// 将需要触发的事件名称, 按照eventSplitter规则解析为一个数组
events = events.split(eventSplitter);
// 将trigger从第2个之后的参数, 记录到rest变量, 将依次传递给回调函数
rest = slice.call(arguments, 1);
// 循环需要触发的事件列表
while( event = events.shift()) {
// 此处的node变量记录了当前事件的所有回调函数列表
if( node = calls[event]) {
// tail变量记录最后一次绑定事件的对象标识
tail = node.tail;
// node变量的值, 按照事件的绑定顺序, 被依次赋值为绑定的单个回调事件对象
// 最后一次绑定的事件next属性, 与tail引用同一个对象, 以此作为是否到达列表末尾的判断依据
while(( node = node.next) !== tail) {
// 执行所有绑定的事件, 并将调用trigger时的参数传递给回调函数
node.callback.apply(node.context || this, rest);
}
}
// 变量all记录了绑定时的"all"事件, 即在调用任何事件时, "all"事件中的回调函数均会被执行
// - "all"事件中的回调函数无论绑定顺序如何, 都会在当前事件的回调函数列表全部执行完毕后再依次执行
// - "all"事件应该在触发普通事件时被自动调用, 如果强制触发"all"事件, 事件中的回调函数将被执行两次
if( node = all) {
tail = node.tail;
// 与调用普通事件的回调函数不同之处在于, all事件会将当前调用的事件名作为第一个参数传递给回调函数
args = [event].concat(rest);
// 遍历并执行"all"事件中的回调函数列表
while(( node = node.next) !== tail) {
node.callback.apply(node.context || this, args);
}
}
}
return this;
}
};
// 绑定事件与释放事件的别名, 也为了同时兼容Backbone以前的版本
Events.bind = Events.on;
Events.unbind = Events.off;
// Backbone.Model 数据对象模型
// --------------
// Model是Backbone中所有数据对象模型的基类, 用于创建一个数据模型
// @param {Object} attributes 指定创建模型时的初始化数据
// @param {Object} options
/**
* @format options
* {
* parse: {Boolean},
* collection: {Collection}
* }
*/
var Model = Backbone.Model = function(attributes, options) {
// defaults变量用于存储模型的默认数据
var defaults;
// 如果没有指定attributes参数, 则设置attributes为空对象
attributes || ( attributes = {});
// 设置attributes默认数据的解析方法, 例如默认数据是从服务器获取(或原始数据是XML格式), 为了兼容set方法所需的数据格式, 可使用parse方法进行解析
if(options && options.parse)
attributes = this.parse(attributes);
if( defaults = getValue(this, 'defaults')) {
// 如果Model在定义时设置了defaults默认数据, 则初始化数据使用defaults与attributes参数合并后的数据(attributes中的数据会覆盖defaults中的同名数据)
attributes = _.extend({}, defaults, attributes);
}
// 显式指定模型所属的Collection对象(在调用Collection的add, push等将模型添加到集合中的方法时, 会自动设置模型所属的Collection对象)
if(options && options.collection)
this.collection = options.collection;
// attributes属性存储了当前模型的JSON对象化数据, 创建模型时默认为空
this.attributes = {};
// 定义_escapedAttributes缓存对象, 它将缓存通过escape方法处理过的数据
this._escapedAttributes = {};
// 为每一个模型配置一个唯一标识
this.cid = _.uniqueId('c');
// 定义一系列用于记录数据状态的对象, 具体含义请参考对象定义时的注释
this.changed = {};
this._silent = {};
this._pending = {};
//创建实例时设置初始化数据, 首次设置使用silent参数, 不会触发change事件
this.set(attributes, {
silent : true
});
// 上面已经设置了初始化数据, changed, _silent, _pending对象的状态可能已经发生变化, 这里重新进行初始化
this.changed = {};
this._silent = {};
this._pending = {};
// _previousAttributes变量存储模型数据的一个副本
// 用于在change事件中获取模型数据被改变之前的状态, 可通过previous或previousAttributes方法获取上一个状态的数据
this._previousAttributes = _.clone(this.attributes);
// 调用initialize初始化方法
this.initialize.apply(this, arguments);
};
// 使用extend方法为Model原型定义一系列属性和方法
_.extend(Model.prototype, Events, {
// changed属性记录了每次调用set方法时, 被改变数据的key集合
changed : null,
// 当指定silent属性时, 不会触发change事件, 被改变的数据会记录下来, 直到下一次触发change事件
// _silent属性用来记录使用silent时的被改变的数据
_silent : null,
_pending : null,
// 每个模型的唯一标识属性(默认为"id", 通过修改idAttribute可自定义id属性名)
// 如果在设置数据时包含了id属性, 则id将会覆盖模型的id
// id用于在Collection集合中查找和标识模型, 与后台接口通信时也会以id作为一条记录的标识
idAttribute : 'id',
// 模型初始化方法, 在模型被构造结束后自动调用
initialize : function() {
},
// 返回当前模型中数据的一个副本(JSON对象格式)
toJSON : function(options) {
return _.clone(this.attributes);
},
// 根据attr属性名, 获取模型中的数据值
get : function(attr) {
return this.attributes[attr];
},
// 根据attr属性名, 获取模型中的数据值, 数据值包含的HTML特殊字符将被转换为HTML实体, 包含 & < > " ' \
// 通过 _.escape方法实现
escape : function(attr) {
var html;
// 从_escapedAttributes缓存对象中查找数据, 如果数据已经被缓存则直接返回
if( html = this._escapedAttributes[attr])
return html;
// _escapedAttributes缓存对象中没有找到数据
// 则先从模型中获取数据
var val = this.get(attr);
// 将数据中的HTML使用 _.escape方法转换为实体, 并缓存到_escapedAttributes对象, 便于下次直接获取
return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
},
// 检查模型中是否存在某个属性, 当该属性的值被转换为Boolean类型后值为false, 则认为不存在
// 如果值为false, null, undefined, 0, NaN, 或空字符串时, 均会被转换为false
has : function(attr) {
return this.get(attr) != null;
},
// 设置模型中的数据, 如果key值不存在, 则作为新的属性添加到模型, 如果key值已经存在, 则修改为新的值
set : function(key, value, options) {
// attrs变量中记录需要设置的数据对象
var attrs, attr, val;
// 参数形式允许key-value对象形式, 或通过key, value两个参数进行单独设置
// 如果key是一个对象, 则认定为使用对象形式设置, 第二个参数将被视为options参数
if(_.isObject(key) || key == null) {
attrs = key;
options = value;
} else {
// 通过key, value两个参数单独设置, 将数据放到attrs对象中方便统一处理
attrs = {};
attrs[key] = value;
}
// options配置项必须是一个对象, 如果没有设置options则默认值为一个空对象
options || ( options = {});
// 没有设置参数时不执行任何动作
if(!attrs)
return this;
// 如果被设置的数据对象属于Model类的一个实例, 则将Model对象的attributes数据对象赋给attrs
// 一般在复制一个Model对象的数据到另一个Model对象时, 会执行该动作
if( attrs instanceof Model)
attrs = attrs.attributes;
// 如果options配置对象中设置了unset属性, 则将attrs数据对象中的所有属性重置为undefined
// 一般在复制一个Model对象的数据到另一个Model对象时, 但仅仅需要复制Model中的数据而不需要复制值时执行该操作
if(options.unset)
for(attr in attrs)
attrs[attr] =
void 0;
// 对当前数据进行验证, 如果验证未通过则停止执行
if(!this._validate(attrs, options))
return false;
// 如果设置的id属性名被包含在数据集合中, 则将id覆盖到模型的id属性
// 这是为了确保在自定义id属性名后, 访问模型的id属性时, 也能正确访问到id
if(this.idAttribute in attrs)
this.id = attrs[this.idAttribute];
var changes = options.changes = {};
// now记录当前模型中的数据对象
var now = this.attributes;
// escaped记录当前模型中通过escape缓存过的数据
var escaped = this._escapedAttributes;
// prev记录模型中数据被改变之前的值
var prev = this._previousAttributes || {};
// 遍历需要设置的数据对象
for(attr in attrs) {
// attr存储当前属性名称, val存储当前属性的值
val = attrs[attr];
// 如果当前数据在模型中不存在, 或已经发生变化, 或在options中指定了unset属性删除, 则删除该数据被换存在_escapedAttributes中的数据
if(!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) {
// 仅删除通过escape缓存过的数据, 这是为了保证缓存中的数据与模型中的真实数据保持同步
delete escaped[attr];
// 如果指定了silent属性, 则此次set方法调用不会触发change事件, 因此将被改变的数据记录到_silent属性中, 便于下一次触发change事件时, 通知事件监听函数此数据已经改变
// 如果没有指定silent属性, 则直接设置changes属性中当前数据为已改变状态
(options.silent ? this._silent : changes)[attr] = true;
}
// 如果在options中设置了unset, 则从模型中删除该数据(包括key)
// 如果没有指定unset属性, 则认为将新增或修改数据, 向模型的数据对象中加入新的数据
options.unset ?
delete now[attr] : now[attr] = val;
// 如果模型中的数据与新的数据不一致, 则表示该数据已发生变化
if(!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
// 在changed属性中记录当前属性已经发生变化的状态
this.changed[attr] = val;
if(!options.silent)
this._pending[attr] = true;
} else {
// 如果数据没有发生变化, 则从changed属性中移除已变化状态
delete this.changed[attr];
delete this._pending[attr];
}
}
// 调用change方法, 将触发change事件绑定的函数
if(!options.silent)
this.change(options);
return this;
},
// 从当前模型中删除指定的数据(属性也将被同时删除)
unset : function(attr, options) {
(options || ( options = {})).unset = true;
// 通过options.unset配置项告知set方法进行删除操作
return this.set(attr, null, options);
},
// 清除当前模型中的所有数据和属性
clear : function(options) {
(options || ( options = {})).unset = true;
// 克隆一个当前模型的属性副本, 并通过options.unset配置项告知set方法执行删除操作
return this.set(_.clone(this.attributes), options);
},
// 从服务器获取默认的模型数据, 获取数据后使用set方法将数据填充到模型, 因此如果获取到的数据与当前模型中的数据不一致, 将会触发change事件
fetch : function(options) {
// 确保options是一个新的对象, 随后将改变options中的属性
options = options ? _.clone(options) : {};
var model = this;
// 在options中可以指定获取数据成功后的自定义回调函数
var success = options.success;
// 当获取数据成功后填充数据并调用自定义成功回调函数
options.success = function(resp, status, xhr) {
// 通过parse方法将服务器返回的数据进行转换
// 通过set方法将转换后的数据填充到模型中, 因此可能会触发change事件(当数据发生变化时)
// 如果填充数据时验证失败, 则不会调用自定义success回调函数
if(!model.set(model.parse(resp, xhr), options))
return false;
// 调用自定义的success回调函数
if(success)
success(model, resp);
};
// 请求发生错误时通过wrapError处理error事件
options.error = Backbone.wrapError(options.error, model, options);
// 调用sync方法从服务器获取数据
return (this.sync || Backbone.sync).call(this, 'read', this, options);
},
// 保存模型中的数据到服务器
save : function(key, value, options) {
// attrs存储需要保存到服务器的数据对象
var attrs, current;
// 支持设置单个属性的方式 key: value
// 支持对象形式的批量设置方式 {key: value}
if(_.isObject(key) || key == null) {
// 如果key是一个对象, 则认为是通过对象方式设置
// 此时第二个参数被认为是options
attrs = key;
options = value;
} else {
// 如果是通过key: value形式设置单个属性, 则直接设置attrs
attrs = {};
attrs[key] = value;
}
// 配置对象必须是一个新的对象
options = options ? _.clone(options) : {};
// 如果在options中设置了wait选项, 则被改变的数据将会被提前验证, 且服务器没有响应新数据(或响应失败)时, 本地数据会被还原为修改前的状态
// 如果没有设置wait选项, 则无论服务器是否设置成功, 本地数据均会被修改为最新状态
if(options.wait) {
// 对需要保存的数据提前进行验证
if(!this._validate(attrs, options))
return false;
// 记录当前模型中的数据, 用于在将数据发送到服务器后, 将数据进行还原
// 如果服务器响应失败或没有返回数据, 则可以保持修改前的状态
current = _.clone(this.attributes);
}
// silentOptions在options对象中加入了silent(不对数据进行验证)
// 当使用wait参数时使用silentOptions配置项, 因为在上面已经对数据进行过验证
// 如果没有设置wait参数, 则仍然使用原始的options配置项
var silentOptions = _.extend({}, options, {
silent : true
});
// 将修改过最新的数据保存到模型中, 便于在sync方法中获取模型数据保存到服务器
if(attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
return false;
}
var model = this;
// 在options中可以指定保存数据成功后的自定义回调函数
var success = options.success;
// 服务器响应成功后执行success
options.success = function(resp, status, xhr) {
// 获取服务器响应最新状态的数据
var serverAttrs = model.parse(resp, xhr);
// 如果使用了wait参数, 则优先将修改后的数据状态直接设置到模型
if(options.wait) {
delete options.wait;
serverAttrs = _.extend(attrs || {}, serverAttrs);
}
// 将最新的数据状态设置到模型中
// 如果调用set方法时验证失败, 则不会调用自定义的success回调函数
if(!model.set(serverAttrs, options))
return false;
if(success) {
// 调用响应成功后自定义的success回调函数
success(model, resp);
} else {
// 如果没有指定自定义回调, 则默认触发sync事件
model.trigger('sync', model, resp, options);
}
};
// 请求发生错误时通过wrapError处理error事件
options.error = Backbone.wrapError(options.error, model, options);
// 将模型中的数据保存到服务器
// 如果当前模型是一个新建的模型(没有id), 则使用create方法(新增), 否则认为是update方法(修改)
var method = this.isNew() ? 'create' : 'update';
var xhr = (this.sync || Backbone.sync).call(this, method, this, options);
// 如果设置了options.wait, 则将数据还原为修改前的状态
// 此时保存的请求还没有得到响应, 因此如果响应失败, 模型中将保持修改前的状态, 如果服务器响应成功, 则会在success中设置模型中的数据为最新状态
if(options.wait)
this.set(current, silentOptions);
return xhr;
},
// 删除模型, 模型将同时从所属的Collection集合中被删除
// 如果模型是在客户端新建的, 则直接从客户端删除
// 如果模型数据同时存在服务器, 则同时会删除服务器端的数据
destroy : function(options) {
// 配置项必须是一个新的对象
options = options ? _.clone(options) : {};
var model = this;
// 在options中可以指定删除数据成功后的自定义回调函数
var success = options.success;
// 删除数据成功调用, 触发destroy事件, 如果模型存在于Collection集合中, 集合将监听destroy事件并在触发时从集合中移除该模型
// 删除模型时, 模型中的数据并没有被清空, 但模型已经从集合中移除, 因此当没有任何地方引用该模型时, 会被自动从内存中释放
// 建议在删除模型时, 将模型对象的引用变量设置为null
var triggerDestroy = function() {
model.trigger('destroy', model, model.collection, options);
};
// 如果该模型是一个客户端新建的模型, 则直接调用triggerDestroy从集合中将模型移除
if(this.isNew()) {
triggerDestroy();
return false;
}
// 当从服务器删除数据成功时
options.success = function(resp) {
// 如果在options对象中配置wait项, 则表示本地内存中的模型数据, 会在服务器数据被删除成功后再删除
// 如果服务器响应失败, 则本地数据不会被删除
if(options.wait)
triggerDestroy();
if(success) {
// 调用自定义的成功回调函数
success(model, resp);
} else {
// 如果没有自定义回调, 则默认触发sync事件
model.trigger('sync', model, resp, options);
}
};
// 请求发生错误时通过wrapError处理error事件
options.error = Backbone.wrapError(options.error, model, options);
// 通过sync方法发送删除数据的请求
var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
// 如果没有在options对象中配置wait项, 则会先删除本地数据, 再发送请求删除服务器数据
// 此时无论服务器删除是否成功, 本地模型数据已被删除
if(!options.wait)
triggerDestroy();
return xhr;
},
// 获取模型在服务器接口中对应的url, 在调用save, fetch, destroy等与服务器交互的方法时, 将使用该方法获取url
// 生成的url类似于"PATHINFO"模式, 服务器对模型的操作只有一个url, 对于修改和删除操作会在url后追加模型id便于标识
// 如果在模型中定义了urlRoot, 服务器接口应为[urlRoot/id]形式
// 如果模型所属的Collection集合定义了url方法或属性, 则使用集合中的url形式: [collection.url/id]
// 在访问服务器url时会在url后面追加上模型的id, 便于服务器标识一条记录, 因此模型中的id需要与服务器记录对应
// 如果无法获取模型或集合的url, 将调用urlError方法抛出一个异常
// 如果服务器接口并没有按照"PATHINFO"方式进行组织, 可以通过重载url方法实现与服务器的无缝交互
url : function() {
// 定义服务器对应的url路径
var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError();
// 如果当前模型是客户端新建的模型, 则不存在id属性, 服务器url直接使用base
if(this.isNew())
return base;
// 如果当前模型具有id属性, 可能是调用了save或destroy方法, 将在base后面追加模型的id
// 下面将判断base最后一个字符是否是"/", 生成的url格式为[base/id]
return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
},
// parse方法用于解析从服务器获取的数据, 返回一个能够被set方法解析的模型数据
// 一般parse方法会根据服务器返回的数据进行重载, 以便构建与服务器的无缝连接
// 当服务器返回的数据结构与set方法所需的数据结构不一致(例如服务器返回XML格式数据时), 可使用parse方法进行转换
parse : function(resp, xhr) {
return resp;
},
// 创建一个新的模型, 它具有和当前模型相同的数据
clone : function() {
return new this.constructor(this.attributes);
},
// 检查当前模型是否是客户端创建的新模型
// 检查方式是根据模型是否存在id标识, 客户端创建的新模型没有id标识
// 因此服务器响应的模型数据中必须包含id标识, 标识的属性名默认为"id", 也可以通过修改idAttribute属性自定义标识
isNew : function() {
return this.id == null;
},
// 数据被更新时触发change事件绑定的函数
// 当set方法被调用, 会自动调用change方法, 如果在set方法被调用时指定了silent配置, 则需要手动调用change方法
change : function(options) {
// options必须是一个对象
options || ( options = {});
// this._changing相关的逻辑有些问题
// this._changing在方法最后被设置为false, 因此方法上面changing变量的值始终为false(第一次为undefined)
// 作者的初衷应该是想用该变量标示change方法是否执行完毕, 对于浏览器端单线程的脚本来说没有意义, 因为该方法被执行时会阻塞其它脚本
// changing获取上一次执行的状态, 如果上一次脚本没有执行完毕, 则值为true
var changing = this._changing;
// 开始执行标识, 执行过程中值始终为true, 执行完毕后this._changing被修改为false
this._changing = true;
// 将非本次改变的数据状态添加到_pending对象中
for(var attr in this._silent)
this._pending[attr] = true;
// changes对象包含了当前数据上一次执行change事件至今, 已被改变的所有数据
// 如果之前使用silent未触发change事件, 则本次会被放到changes对象中
var changes = _.extend({}, options.changes, this._silent);
// 重置_silent对象
this._silent = {};
// 遍历changes对象, 分别针对每一个属性触发单独的change事件
for(var attr in changes) {
// 将Model对象, 属性值, 配置项作为参数以此传递给事件的监听函数
this.trigger('change:' + attr, this, this.get(attr), options);
}
// 如果方法处于执行中, 则停止执行
if(changing)
return this;
// 触发change事件, 任意数据被改变后, 都会依次触发"change:属性"事件和"change"事件
while(!_.isEmpty(this._pending)) {
this._pending = {};
// 触发change事件, 并将Model实例和配置项作为参数传递给监听函数
this.trigger('change', this, options);
// 遍历changed对象中的数据, 并依次将已改变数据的状态从changed中移除
// 在此之后如果调用hasChanged检查数据状态, 将得到false(未改变)
for(var attr in this.changed) {
if(this._pending[attr] || this._silent[attr])
continue;
// 移除changed中数据的状态
delete this.changed[attr];
}
// change事件执行完毕, _previousAttributes属性将记录当前模型最新的数据副本
// 因此如果需要获取数据的上一个状态, 一般只通过在触发的change事件中通过previous或previousAttributes方法获取
this._previousAttributes = _.clone(this.attributes);
}
// 执行完毕标识
this._changing = false;
return this;
},
// 检查某个数据是否在上一次执行change事件后被改变过
/**
* 一般在change事件中配合previous或previousAttributes方法使用, 如:
* if(model.hasChanged('attr')) {
* var attrPrev = model.previous('attr');
* }
*/
hasChanged : function(attr) {
if(!arguments.length)
return !_.isEmpty(this.changed);
return _.has(this.changed, attr);
},
// 获取当前模型中的数据与上一次数据中已经发生变化的数据集合
// (一般在使用silent属性时没有调用change方法, 因此数据会被临时抱存在changed属性中, 上一次的数据可通过previousAttributes方法获取)
// 如果传递了diff集合, 将使用上一次模型数据与diff集合中的数据进行比较, 返回不一致的数据集合
// 如果比较结果中没有差异, 则返回false
changedAttributes : function(diff) {
// 如果没有指定diff, 将返回当前模型较上一次状态已改变的数据集合, 这些数据已经被存在changed属性中, 因此返回changed集合的一个副本
if(!diff)
return this.hasChanged() ? _.clone(this.changed) : false;
// 指定了需要进行比较的diff集合, 将返回上一次的数据与diff集合的比较结果
// old变量存储了上一个状态的模型数据
var val, changed = false, old = this._previousAttributes;
// 遍历diff集合, 并将每一项与上一个状态的集合进行比较
for(var attr in diff) {
// 将比较结果不一致的数据临时存储到changed变量
if(_.isEqual(old[attr], ( val = diff[attr])))
continue;
(changed || (changed = {}))[attr] = val;
}
// 返回比较结果
return changed;
},
// 在模型触发的change事件中, 获取某个属性被改变前上一个状态的数据, 一般用于进行数据比较或回滚
// 该方法一般在change事件中调用, change事件被触发后, _previousAttributes属性存放最新的数据
previous : function(attr) {
// attr指定需要获取上一个状态的属性名称
if(!arguments.length || !this._previousAttributes)
return null;
return this._previousAttributes[attr];
},
// 在模型触发change事件中, 获取所有属性上一个状态的数据集合
// 该方法类似于previous()方法, 一般在change事件中调用, 用于数据比较或回滚
previousAttributes : function() {
// 将上一个状态的数据对象克隆为一个新对象并返回
return _.clone(this._previousAttributes);
},
// Check if the model is currently in a valid state. It's only possible to
// get into an *invalid* state if you're using silent changes.
// 验证当前模型中的数据是否能通过validate方法验证, 调用前请确保定义了validate方法
isValid : function() {
return !this.validate(this.attributes);
},
// 数据验证方法, 在调用set, save, add等数据更新方法时, 被自动执行
// 验证失败会触发模型对象的"error"事件, 如果在options中指定了error处理函数, 则只会执行options.error函数
// @param {Object} attrs 数据模型的attributes属性, 存储模型的对象化数据
// @param {Object} options 配置项
// @return {Boolean} 验证通过返回true, 不通过返回false
_validate : function(attrs, options) {
// 如果在调用set, save, add等数据更新方法时设置了options.silent属性, 则忽略验证
// 如果Model中没有添加validate方法, 则忽略验证
if(options.silent || !this.validate)
return true;
// 获取对象中所有的属性值, 并放入validate方法中进行验证
// validate方法包含2个参数, 分别为模型中的数据集合与配置对象, 如果验证通过则不返回任何数据(默认为undefined), 验证失败则返回带有错误信息数据
attrs = _.extend({}, this.attributes, attrs);
var error = this.validate(attrs, options);
// 验证通过
if(!error)
return true;
// 验证未通过
// 如果配置对象中设置了error错误处理方法, 则调用该方法并将错误数据和配置对象传递给该方法
if(options && options.error) {
options.error(this, error, options);
} else {
// 如果对模型绑定了error事件监听, 则触发绑定事件
this.trigger('error', this, error, options);
}
// 返回验证未通过标识
return false;
}
});
// Backbone.Collection 数据模型集合相关
// -------------------
// Collection集合存储一系列相同类的数据模型, 并提供相关方法对模型进行操作
var Collection = Backbone.Collection = function(models, options) {
// 配置对象
options || ( options = {});
// 在配置参数中设置集合的模型类
if(options.model)
this.model = options.model;
// 如果设置了comparator属性, 则集合中的数据将按照comparator方法中的排序算法进行排序(在add方法中会自动调用)
if(options.comparator)
this.comparator = options.comparator;
// 实例化时重置集合的内部状态(第一次调用时可理解为定义状态)
this._reset();
// 调用自定义初始化方法, 如果需要一般会重载initialize方法
this.initialize.apply(this, arguments);
// 如果指定了models数据, 则调用reset方法将数据添加到集合中
// 首次调用时设置了silent参数, 因此不会触发"reset"事件
if(models)
this.reset(models, {
silent : true,
parse : options.parse
});
};
// 通过extend方法定义集合类原型方法
_.extend(Collection.prototype, Events, {
// 定义集合的模型类, 模型类必须是一个Backbone.Model的子类
// 在使用集合相关方法(如add, create等)时, 允许传入数据对象, 集合方法会根据定义的模型类自动创建对应的实例
// 集合中存储的数据模型应该都是同一个模型类的实例
model : Model,
// 初始化方法, 该方法在集合实例被创建后自动调用
// 一般会在定义集合类时重载该方法
initialize : function() {
},
// 返回一个数组, 包含了集合中每个模型的数据对象
toJSON : function(options) {
// 通过Undersocre的map方法将集合中每一个模型的toJSON结果组成一个数组, 并返回
return this.map(function(model) {
// 依次调用每个模型对象的toJSON方法, 该方法默认将返回模型的数据对象(复制的副本)
// 如果需要返回字符串等其它形式, 可以重载toJSON方法
return model.toJSON(options);
});
},
// 向集合中添加一个或多个模型对象
// 默认会触发"add"事件, 如果在options中设置了silent属性, 可以关闭此次事件触发
// 传入的models可以是一个或一系列的模型对象(Model类的实例), 如果在集合中设置了model属性, 则允许直接传入数据对象(如 {name: 'test'}), 将自动将数据对象实例化为model指向的模型对象
add : function(models, options) {
// 局部变量定义
var i, index, length, model, cid, id, cids = {}, ids = {}, dups = [];
options || ( options = {});
// models必须是一个数组, 如果只传入了一个模型, 则将其转换为数组
models = _.isArray(models) ? models.slice() : [models];
// 遍历需要添加的模型列表, 遍历过程中, 将执行以下操作:
// - 将数据对象转化模型对象
// - 建立模型与集合之间的引用
// - 记录无效和重复的模型, 并在后面进行过滤
for( i = 0, length = models.length; i < length; i++) {
// 将数据对象转换为模型对象, 简历模型与集合的引用, 并存储到model(同时models中对应的模型已经被替换为模型对象)
if(!( model = models[i] = this._prepareModel(models[i], options))) {
throw new Error("Can't add an invalid model to a collection");
}
// 当前模型的cid和id
cid = model.cid;
id = model.id;
// dups数组中记录了无效或重复的模型索引(models数组中的索引), 并在下一步进行过滤删除
// 如果cids, ids变量中已经存在了该模型的索引, 则认为是同一个模型在传入的models数组中声明了多次
// 如果_byCid, _byId对象中已经存在了该模型的索引, 则认为同一个模型在当前集合中已经存在
// 对于上述两种情况, 将模型的索引记录到dups进行过滤删除
if(cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) {
dups.push(i);
continue;
}
// 将models中已经遍历过的模型记录下来, 用于在下一次循环时进行重复检查
cids[cid] = ids[id] = model;
}
// 从models中删除无效或重复的模型, 保留目前集合中真正需要添加的模型列表
i = dups.length;
while(i--) {
models.splice(dups[i], 1);
}
// 遍历需要添加的模型, 监听模型事件并记录_byCid, _byId列表, 用于在调用get和getByCid方法时作为索引
for( i = 0, length = models.length; i < length; i++) {
// 监听模型中的所有事件, 并执行_onModelEvent方法
// _onModelEvent方法中会对模型抛出的add, remove, destroy和change事件进行处理, 以便模型与集合中的状态保持同步
( model = models[i]).on('all', this._onModelEvent, this);
// 将模型根据cid记录到_byCid对象, 便于根据cid进行查找
this._byCid[model.cid] = model;
// 将模型根据id记录到_byId对象, 便于根据id进行查找
if(model.id != null)
this._byId[model.id] = model;
}
// 改变集合的length属性, length属性记录了当前集合中模型的数量
this.length += length;
// 设置新模型列表插入到集合中的位置, 如果在options中设置了at参数, 则在集合的at位置插入
// 默认将插入到集合的末尾
// 如果设置了comparator自定义排序方法, 则设置at后还将按照comparator中的方法进行排序, 因此最终的顺序可能并非在at指定的位置
index = options.at != null ? options.at : this.models.length;
splice.apply(this.models, [index, 0].concat(models));
// 如果设置了comparator方法, 则将数据按照comparator中的算法进行排序
// 自动排序使用silent属性阻止触发reset事件
if(this.comparator)
this.sort({
silent : true
});
// 依次对每个模型对象触发"add"事件, 如果设置了silent属性, 则阻止事件触发
if(options.silent)
return this;
// 遍历新增加的模型列表
for( i = 0, length = this.models.length; i < length; i++) {
if(!cids[( model = this.models[i]).cid])
continue;
options.index = i;
// 触发模型的"add"事件, 因为集合监听了模型的"all"事件, 因此在_onModelEvent方法中, 集合也将触发"add"事件
// 详细信息可参考Collection.prototype._onModelEvent方法
model.trigger('add', model, this, options);
}
return this;
},
// 从集合中移除模型对象(支持移除多个模型)
// 传入的models可以是需要移除的模型对象, 或模型的cid和模型的id
// 移除模型并不会调用模型的destroy方法
// 如果没有设置options.silent参数, 将触发模型的remove事件, 同时将触发集合的remove事件(集合通过_onModelEvent方法监听了模型的所有事件)
remove : function(models, options) {
var i, l, index, model;
// options默认为空对象
options || ( options = {});
// models必须是数组类型, 当只移除一个模型时, 将其放入一个数组
models = _.isArray(models) ? models.slice() : [models];
// 遍历需要移除的模型列表
for( i = 0, l = models.length; i < l; i++) {
// 所传入的models列表中可以是需要移除的模型对象, 或模型的cid和模型的id
// (在getByCid和get方法中, 可通过cid, id来获取模型, 如果传入的是一个模型对象, 则返回模型本身)
model = this.getByCid(models[i]) || this.get(models[i]);
// 没有获取到模型
if(!model)
continue;
// 从_byId列表中移除模型的id引用
delete this._byId[model.id];
// 从_byCid列表中移除模型的cid引用
delete this._byCid[model.cid];
// indexOf是Underscore对象中的方法, 这里通过indexOf方法获取模型在集合中首次出现的位置
index = this.indexOf(model);
// 从集合列表中移除该模型
this.models.splice(index, 1);
// 重置当前集合的length属性(记录集合中模型的数量)
this.length--;
// 如果没有设置silent属性, 则触发模型的remove事件
if(!options.silent) {
// 将当前模型在集合中的位置添加到options对象并传递给remove监听事件, 以便在事件函数中可以使用
options.index = index;
model.trigger('remove', model, this, options);
}
// 解除模型与集合的关系, 包括集合中对模型的引用和事件监听
this._removeReference(model);
}
return this;
},
// 向集合的末尾添加模型对象
// 如果集合类中定义了comparator排序方法, 则通过push方法添加的模型将按照comparator定义的算法进行排序, 因此模型顺序可能会被改变
push : function(model, options) {
// 通过_prepareModel方法将model实例化为模型对象, 这句代码是多余的, 因为在下面调用的add方法中还会通过_prepareModel获取一次模型
model = this._prepareModel(model, options);
// 调用add方法将模型添加到集合中(默认添加到集合末尾)
this.add(model, options);
return model;
},
// 移除集合中最后一个模型对象
pop : function(options) {
// 获取集合中最后一个模型
var model = this.at(this.length - 1);
// 通过remove方法移除该模型
this.remove(model, options);
return model;
},
// 向集合的第一个位置插入模型
// 如果集合类中定义了comparator排序方法, 则通过unshift方法添加的模型将按照comparator定义的算法进行排序, 因此模型顺序可能会被改变
unshift : function(model, options) {
// 通过_prepareModel方法将model实例化为模型对象
model = this._prepareModel(model, options);
// 调用add方法将模型插入到集合的第一个位置(设置at为0)
// 如果定义了comparator排序方法, 集合的顺序将被重排
this.add(model, _.extend({
at : 0
}, options));
return model;
},
// 移除并返回集合中的第一个模型对象
shift : function(options) {
// 获得集合中的第一个模型
var model = this.at(0);
// 从集合中删除该模型
this.remove(model, options);
// 返回模型对象
return model;
},
// 根据id从集合中查找模型并返回
get : function(id) {
if(id == null)
return
void 0;
return this._byId[id.id != null ? id.id : id];
},
// 根据cid从集合中查找模型并返回
getByCid : function(cid) {
return cid && this._byCid[cid.cid || cid];
},
// 根据索引(下标, 从0开始)从集合中查找模型并返回
at : function(index) {
return this.models[index];
},
// 对集合中的模型根据值进行筛选
// attrs是一个筛选对象, 如 {name: 'Jack'}, 将返回集合中所有name为"Jack"的模型(数组)
where : function(attrs) {
// attrs不能为空值
if(_.isEmpty(attrs))
return [];
// 通过filter方法对集合中的模型进行筛选
// filter方法是Underscore中的方法, 用于将遍历集合中的元素, 并将能通过处理器验证(返回值为true)的元素作为数组返回
return this.filter(function(model) {
// 遍历attrs对象中的验证规则
for(var key in attrs) {
// 将attrs中的验证规则与集合中的模型进行匹配
if(attrs[key] !== model.get(key))
return false;
}
return true;
});
},
// 对集合中的模型按照comparator属性指定的方法进行排序
// 如果没有在options中设置silent参数, 则排序后将触发reset事件
sort : function(options) {
// options默认是一个对象
options || ( options = {});
// 调用sort方法必须指定了comparator属性(排序算法方法), 否则将抛出一个错误
if(!this.comparator)
throw new Error('Cannot sort a set without a comparator');
// boundComparator存储了绑定当前集合上下文对象的comparator排序算法方法
var boundComparator = _.bind(this.comparator, this);
if(this.comparator.length == 1) {
this.models = this.sortBy(boundComparator);
} else {
// 调用Array.prototype.sort通过comparator算法对数据进行自定义排序
this.models.sort(boundComparator);
}
// 如果没有指定silent参数, 则触发reset事件
if(!options.silent)
this.trigger('reset', this, options);
return this;
},
// 将集合中所有模型的attr属性值存放到一个数组并返回
pluck : function(attr) {
// map是Underscore中的方法, 用于遍历一个集合, 并将所有处理器的返回值作为一个数组返回
return _.map(this.models, function(model) {
// 返回当前模型的attr属性值
return model.get(attr);