forked from jashkenas/backbone
-
Notifications
You must be signed in to change notification settings - Fork 8
/
backbone.js
2013 lines (1764 loc) · 91.8 KB
/
backbone.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 1.1.2
// (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
/**
* AMD规范支持,这里的root指的是window
*/
(function (root, factory) {
// Set up Backbone appropriately for the environment. Start with AMD.
if (typeof define === 'function' && define.amd) {
define(['underscore', 'jquery', 'exports'], function (_, $, exports) {
// Export global even in AMD case in case this script is loaded with
// others that may still expect a global Backbone.
root.Backbone = factory(root, exports, _, $);
});
// Next for Node.js or CommonJS. jQuery may not be needed as a module.
} else if (typeof exports !== 'undefined') {
var _ = require('underscore');
factory(root, exports, _);
// Finally, as a browser global.
} else {
root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
}
}(this, function (root, Backbone, _, $) {
// Initial Setup
// -------------
// Save the previous value of the `Backbone` variable, so that it can be
// restored later on, if `noConflict` is used.
//保存上一个版本的backbone,因为有可能页面引入了多次backbone
var previousBackbone = root.Backbone;
// Create local references to array methods we'll want to use later.
//用局部变量保存数组常用的方法,
//slice 一般用于将伪数组转成真正的数组,比如arguments
var array = [];
var slice = array.slice;
// Current version of the library. Keep in sync with `package.json`.
//版本号
Backbone.VERSION = '1.1.2';
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
// the `$` variable.
//jq或 zepto等dom操作类库支持
Backbone.$ = $;
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
// to its previous owner. Returns a reference to this Backbone object.
// 在 无冲突 模式下运行Backbone
// 用一个引用指向先前 Backbone 的对象,返回变量 `Backbone`
Backbone.noConflict = function () {
root.Backbone = previousBackbone;
return this;
};
// Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
// will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
// set a `X-Http-Method-Override` header.
// 开启 `emulateHTTP`(模拟HTTP) 以支持传统的 HTTP 服务.
// 通过设置 '_method' 参数和设置 'X-Http-Method-Override' 头的这个参数可以伪造 'PUT'和'DELETE' 请求
// 简单的说 就是 老的浏览器不支持REST/HTTP,开启emulateHTTP选项,Backbone将通过POST方法伪造PUT和DELETE方法
Backbone.emulateHTTP = false;
// Turn on `emulateJSON` to support legacy servers that can't deal with direct
// `application/json` requests ... will encode the body as
// `application/x-www-form-urlencoded` instead and will send the model in a
// form param named `model`.
// 开启 `emulateJSON`(模拟JSON) 以支持传统服务器无法直接处理的问题
// `application/json` 请求 ... 将自身编码为application/x-www-form-urlencoded`发送模型, 而不是直接'model'的表单参数。
// 老的浏览器不支持直接发送'application/json'编码的请求,开启emulateJSON选项
// Backbone会将JSON模型数据序列化为modal参数,请求会按照application/x-www-form-urlencoded的内容发送
Backbone.emulateJSON = false;
// Backbone.Events
// ---------------
// A module that can be mixed in to *any object* in order to provide it with
// custom events. You may bind with `on` or remove with `off` callback
// functions to an event; `trigger`-ing an event fires all callbacks in
// succession.
//
// var object = {};
// _.extend(object, Backbone.Events);
// object.on('expand', function(){ alert('expanded'); });
// object.trigger('expand');
//
// TODO 暂时跳过
var Events = Backbone.Events = {
// Bind an event to a `callback` function. Passing `"all"` will bind
// the callback to all events fired.
// 对一个事件绑定回调函数. 传递 "all" 则将绑定回调在所有事件触发时调用
on: function (name, callback, context) {
// API 检测
if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
//相当于 this._events = this._events || {} 下面语法更容易用来赋值 如下下句
//在绑定时间的对象中 建立事件池 来进行事件管理
this._events || (this._events = {});
// name 事件在事件池中的形式(数组形式) 存放当前对象绑定在name的所有事件
var events = this._events[name] || (this._events[name] = []);
// 将当前需要绑定的事件 push到事件池中的具体事件名称中
events.push({callback: callback, context: context, ctx: context || this});
return this;
},
// Bind an event to only be triggered a single time. After the first time
// the callback is invoked, it will be removed.
// 绑定一个只触发一次的事件. 当回调被触发后即便被解绑删除
once: function (name, callback, context) {
if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
var self = this;
// 自定义一个 once 事件
var once = _.once(function () {
self.off(name, once);
callback.apply(this, arguments);
});
once._callback = callback;
return this.on(name, once, context);
},
// Remove one or many callbacks. If `context` is null, removes all
// callbacks with that function. If `callback` is null, removes all
// callbacks for the event. If `name` is null, removes all bound
// callbacks for all events.
// 解绑一个或者多个回调.
// 如果 `context` 是 null, 移除所有有该函数的回调.
// 如果 `callback` 是 null, 移除该事件下的所有回调.
// 如果 `name` 是 null, 移除所有绑定的回调函数
off: function (name, callback, context) {
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
// Remove all callbacks for all events.
//当name,callback,context都没指定时,则移除所有事件
if (!name && !callback && !context) {
//void 0 相当于一个空对象 {}
this._events = void 0;
return this;
}
//未指定name时,则取所有的事件name
//使用 underscore 的 keys 获取对象键值方法
var names = name ? [name] : _.keys(this._events);
//对每个包含回调函数的对象进行筛选,不符合指定参数条件的进行保留
for (var i = 0, length = names.length; i < length; i++) {
name = names[i];
// Bail out if there are no events stored.
var events = this._events[name];
if (!events) continue;
// Remove all callbacks for this event.
if (!callback && !context) {
delete this._events[name];
continue;
}
// Find any remaining events.
var remaining = [];
for (var j = 0, k = events.length; j < k; j++) {
var event = events[j];
if (
callback && callback !== event.callback &&
callback !== event.callback._callback ||
context && context !== event.context
) {
remaining.push(event);
}
}
// Replace events if there are any remaining. Otherwise, clean up.
if (remaining.length) {
this._events[name] = remaining;
} else {
delete this._events[name];
}
}
return this;
},
// Trigger one or many events, firing all bound callbacks. Callbacks are
// passed the same arguments as `trigger` is, apart from the event name
// (unless you're listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
// 触发一个或多个事件, 触发所有绑定的回调.
// 除了事件名称,回调函数会被传递'trigger'相同的参数。
// (如果你监听了 'all', 会让你的回调函数将事件名称作为第一个参数).
// obj.trigger("change",function(){});
// obj.trigger("all",function(eventName){ alert(eventName) });
trigger: function (name) {
if (!this._events) return this;
//参数列表,不包含name 即不包含第一个tigger的参数 name
var args = slice.call(arguments, 1);
// trigger方法中调用了两次triggerEvents
if (!eventsApi(this, 'trigger', name, args)) return this;
var events = this._events[name];
var allEvents = this._events.all;
//通过参数传进来的事件触发
if (events) triggerEvents(events, args);
//通过固定的“all”事件触发。
if (allEvents) triggerEvents(allEvents, arguments);
return this;
},
// Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to.
// 使这个对象或者所有监听特定事件的对象停止监听该特定的事件
stopListening: function (obj, name, callback) {
var listeningTo = this._listeningTo;
if (!listeningTo) return this;
var remove = !name && !callback;
if (!callback && typeof name === 'object') callback = this;
if (obj) (listeningTo = {})[obj._listenId] = obj;
for (var id in listeningTo) {
obj = listeningTo[id];
obj.off(name, callback, this);
if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id];
}
return this;
}
};
// Regular expression used to split event strings.
// 用于切割多个事件的正则,比如'click mouseover'
var eventSplitter = /\s+/;
// Implement fancy features of the Events API such as multiple event
// names `"change blur"` and jQuery-style event maps `{change: action}`
// in terms of the existing API.
// 处理多个事件的绑定,比如"change blur"或者'{chage: action}'
//该函数的主要作用:
//当指定事件名为object对象时,将object对象中key作为事件名
//将obejct中的value作为回调函数对象,然后递归调用on、off、trigger
//当指定的事件名为string,但包含空格时,将string按空格切割,再依次递归调用
//该函数需要对应的看它是如何被调用的,直接看是比较难明白的
var eventsApi = function (obj, action, name, rest) {
if (!name) return true;
// Handle event maps.
//当指定的事件名为object时
if (typeof name === 'object') {
for (var key in name) {
obj[action].apply(obj, [key, name[key]].concat(rest));
}
return false;
}
// Handle space separated event names.
// 当指定的事件名包含空格时
if (eventSplitter.test(name)) {
var names = name.split(eventSplitter);
for (var i = 0, length = names.length; i < length; i++) {
obj[action].apply(obj, [names[i]].concat(rest));
}
return false;
}
return true;
};
// A difficult-to-believe, but optimized internal dispatch function for
// triggering events. Tries to keep the usual cases speedy (most internal
// Backbone events have 3 arguments).
//调用事件回调函数的函数
//为了性能问题,才使用了switch,而不是直接使用default中的代码
// 关于这个函数性能测试 可以查看下:http://jsperf.com/js-function-call-vs-function-apply
//实际上是call 与 apply的性能差异 http://stackoverflow.com/questions/14968387/backbone-triggerevents-variable
//中文:http://www.cnblogs.com/snandy/archive/2013/05/23/3091258.html
var triggerEvents = function (events, args) {
var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
switch (args.length) {
case 0:
while (++i < l) (ev = events[i]).callback.call(ev.ctx);
return;
case 1:
while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1);
return;
case 2:
while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2);
return;
case 3:
while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3);
return;
default:
while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
return;
}
};
// 给一个对象监听另一个对象的某一事件的能力,当另一个对象的某个事件触发时,将会执行当前这个对象的回调函数
var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
// Inversion-of-control versions of `on` and `once`. Tell *this* object to
// listen to an event in another object ... keeping track of what it's
// listening to.
_.each(listenMethods, function (implementation, method) {
Events[method] = function (obj, name, callback) {
var listeningTo = this._listeningTo || (this._listeningTo = {});
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
listeningTo[id] = obj;
if (!callback && typeof name === 'object') callback = this;
obj[implementation](name, callback, this);
return this;
};
});
// Aliases for backwards compatibility.
//向后兼容,以前的版本用bind来绑定事件
Events.bind = Events.on;
Events.unbind = Events.off;
// Allow the `Backbone` object to serve as a global event bus, for folks who
// want global "pubsub" in a convenient place.
// 将Events对象fix到Backbone对象
_.extend(Backbone, Events);
//在Events中维护了一个_events对象,而_events对象中的属性名就代表了一个事件,属性值为一个数组,数组中的元素为包含了注册的回调函数的对象。
// 所以当我们调用on('click', callback, ctx)时,实际上是做了这样的操作:this._events['click'].push({ callback: callback, context: ctx });
//
//
// Backbone.Model
// --------------
// Backbone **Models** are the basic data object in the framework --
// frequently representing a row in a table in a database on your server.
// A discrete chunk of data and a bunch of useful, related methods for
// performing computations and transformations on that data.
// Create a new model with the specified attributes. A client id (`cid`)
// is automatically generated and assigned for you.
// Backbone **Models** 在框架中是基础的数据对象 --
// 常常代表你服务器数据库表中的一行.
// 一个离散的数据块,和一堆对这些数据进行计算转换的有用的相关的方法
// 使用指定的属性创建一个新的模型.
// 会自动生成并分配一个用户id (`cid`)
var Model = Backbone.Model = function (attributes, options) {
// debugger;
//设置model的默认参数,如果不传的话,默认都是空对象
var attrs = attributes || {}; //attrs也相当于是model的实例属性,一般对应于数据库的字段
options || (options = {});// 配置项
//创建一个页面内唯一的一个uid
this.cid = _.uniqueId('c');
this.attributes = {};
if (options.collection) this.collection = options.collection;
//解析attrs,默认直接返回attrs
if (options.parse) attrs = this.parse(attrs, options) || {};
attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
//设置model相应的属性
this.set(attrs, options);
this.changed = {};
//初始化事件,可以覆盖重写这个方法,this指向model
this.initialize.apply(this, arguments);
};
// Attach all inheritable methods to the Model prototype.
//给 prototype 添加 属性
// 将所有可继承的方法添加到 模型 的原型中.
_.extend(Model.prototype, Events, {
// A hash of attributes whose current and previous value differ.
// 记录当前数据对象下哪些属性改变了
changed: null,
// The value returned during the last failed validation.
// 最后一次验证失败的返回值.
validationError: null,
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
// CouchDB users may want to set this to `"_id"`.
// 数据的id属性模式是什么
//一般对应数据库的自增长id,如果和数据库上的id名称不一样,比如page表的id叫page_id,则通过这个属性进行修改
idAttribute: 'id',
// Initialize is an empty function by default. Override it with your own
// initialization logic.
// 初始化函数
initialize: function () {
},
// Return a copy of the model's `attributes` object.
// 返回一个模型的属性对象的拷贝.
toJSON: function (options) {
return _.clone(this.attributes);
},
// Proxy `Backbone.sync` by default -- but override this if you need
// custom syncing semantics for *this* particular model.
// 默认使用 `Backbone.sync` 代理
// 如果需要可以自定义从写
//该方法用于向服务端同步数据(增、删、改)
//该方法默认调用的是Backbone.sync方法(ajax)
//我们可以通过替换Backbone.sync来使用我们自己的sync方法,比如mongodb,这样backbone也能用于Node.js后端
sync: function () {
return Backbone.sync.apply(this, arguments);
},
// Get the value of an attribute.
// 获取属性值
get: function (attr) {
return this.attributes[attr];
},
// Get the HTML-escaped value of an attribute.
// 获取属性的 HTML-escaped 值. 转义HTML字符串
escape: function (attr) {
return _.escape(this.get(attr));
},
// Returns `true` if the attribute contains a value that is not null
// or undefined.
// 若属性值不为 null 或者 undefined 则返回 `true`
// 判断属性是否存在
has: function (attr) {
return this.get(attr) != null;
},
// Set a hash of model attributes on the object, firing `"change"`. This is
// the core primitive operation of a model, updating the data and notifying
// anyone who needs to know about the change in state. The heart of the beast.
// 在对象上建立模型的属性哈希, 触发 `"change"`.
// 这是 模型 的核心操作, 更新数据, 通知那些需要知道 模型 状态变化的对象
// backbone 野兽的心脏.
// 第一个参数key如果是对象类型,直接将该对象拷贝到this.attributes上,
// 如果key是字符串,那么将在内部把key,value转成一个临时对象attrs再拷贝到this.attributes上。
// jQuery API 的风格
set: function (key, val, options) {
var attr, attrs, unset, changes, silent, changing, prev, current;
if (key == null) return this;
// Handle both `"key", value` and `{key: value}` -style arguments.
// 处理 `"key", value` 和 `{key: value}` 2种参数形式的情况.
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options || (options = {});
// Run validation.
// 执行验证.
// 验证设置的属性是否符合要求,_validate方法内部将会调用validate方法
// validate方法需要model使用者自己指定
// 当设置的属性不符合要求时,直接返回false
if (!this._validate(attrs, options)) return false;
// Extract attributes and options.
// 提取 属性 和 可选项.
//表示应当删除属性,而不是设置属性
unset = options.unset;
//当silent为true时,不触发change事件
silent = options.silent;
//变化的属性列表
changes = [];
//表示是否在变化中
changing = this._changing;
this._changing = true;
if (!changing) {
//表示变化前的属性值
this._previousAttributes = _.clone(this.attributes);
//存储改变了的属性和其属性值
this.changed = {};
}
//通过current,prev两个参数存储当前值和原始值
current = this.attributes, prev = this._previousAttributes;
// Check for changes of `id`.
// 检测 `id` 变化.
// 如果设置了idAttribute为别的id ,则内部设置一个id来表示这个model
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
// For each `set` attribute, update or delete the current value.
// 循环设置model属性值
for (attr in attrs) {
val = attrs[attr];
//当设置的值与当前的model对象的属性值不同时,将要设置的属性的key加入changes列表中
if (!_.isEqual(current[attr], val)) changes.push(attr);
//this.changed存储改变了的属性和其属性值
if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val;
} else {
delete this.changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
}
// Trigger all relevant attribute changes.
// 触发change事件
if (!silent) {
if (changes.length) this._pending = options;
//触发'change:变更的属性名'事件
for (var i = 0, length = changes.length; i < length; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
// while 循环 修改将被递归嵌套在'events'事件中
if (changing) return this;
if (!silent) {
//触发'change'事件,这里使用while,是因为change事件也有可能会调用set方法
//所以需要递归的调用
while (this._pending) {
options = this._pending;
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
this._changing = false;
return this;
},
// Remove an attribute from the model, firing `"change"`. `unset` is a noop
// if the attribute doesn't exist.
// 从模型中移除一个属性, 触发 `"change"`.
// `unset` 如果属性不存在设置为空
unset: function (attr, options) {
return this.set(attr, void 0, _.extend({}, options, {unset: true}));
},
// Clear all attributes on the model, firing `"change"`.
// 清空模型中的所有属性,触发 `"change"`.
clear: function (options) {
var attrs = {};
for (var key in this.attributes) attrs[key] = void 0;
return this.set(attrs, _.extend({}, options, {unset: true}));
},
// Determine if the model has changed since the last `"change"` event.
// If you specify an attribute name, determine if that attribute has changed.
// 确保 模型 在上一次更改后再一次更改 `"change"` event.
// 如果指定了属性名称, 确定属性已经改变.
hasChanged: function (attr) {
if (attr == null) return !_.isEmpty(this.changed);
return _.has(this.changed, attr);
},
// Return an object containing all the attributes that have changed, or
// false if there are no changed attributes. Useful for determining what
// parts of a view need to be updated and/or what attributes need to be
// persisted to the server. Unset attributes will be set to undefined.
// You can also pass an attributes object to diff against the model,
// determining if there *would be* a change.
// 返回一个包含所有改变的属性的对象, 当没有属性被更改,就返回 false.
// 常用来判断视图块是否需要更新或者那些属性需要保存到后端
// 未设置的 属性将设置为 undefined.
// 你也可以针对模型传递一个属性对象来改变,决定是否 *would be* 改变.
changedAttributes: function (diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var val, changed = false;
var old = this._changing ? this._previousAttributes : this.attributes;
for (var attr in diff) {
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
(changed || (changed = {}))[attr] = val;
}
return changed;
},
// Get the previous value of an attribute, recorded at the time the last
// `"change"` event was fired.
// 获取一个属性之前,在最后一次 'change' 事件触发时候的值
previous: function (attr) {
if (attr == null || !this._previousAttributes) return null;
return this._previousAttributes[attr];
},
// Get all of the attributes of the model at the time of the previous
// `"change"` event.
// 获取上一次 `"change"` 事件时所有属性的值.
previousAttributes: function () {
return _.clone(this._previousAttributes);
},
// Fetch the model from the server. If the server's representation of the
// model differs from its current attributes, they will be overridden,
// triggering a `"change"` event.
// 从服务器端获取 模型 . 相当于ajax的get操作
// 如果服务器端的显示的模型与当前属性有区别,那么覆盖并且触发事件"change"`.
fetch: function (options) {
options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true;
var model = this;
//先缓存用户设置的success
var success = options.success;
//fetch方法自带的success回调
options.success = function (resp) {
//从服务器获取数据后设置model的属性值
if (!model.set(model.parse(resp, options), options)) return false;
if (success) success(model, resp, options);
//触发sync事件
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
return this.sync('read', this, options);
},
// Set a hash of model attributes, and sync the model to the server.
// If the server returns an attributes hash that differs, the model's
// state will be `set` again.
// 设置属性的哈希, 同步模型到服务器.
// 如果服务器返回一个有区别的属性散列,那么模型的状态要重新设置
save: function (key, val, options) {
var attrs, method, xhr, attributes = this.attributes;
// Handle both `"key", value` and `{key: value}` -style arguments.
// 处理 `"key", value` and `{key: value}` 2种参数情况.
if (key == null || typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
//默认验证是通过的 validate true
options = _.extend({validate: true}, options);
// If we're not waiting and attributes exist, save acts as
// `set(attr).save(null, opts)` with validation. Otherwise, check if
// the model will be valid when the attributes, if any, are set.
// 如果并不在等待队列中并且属性不存在, 保存行为以 `set(attr).save(null, opts)`格式.
// 如果选项参数里面没有wait方法 即不需要验证,直接就set了
// options.wait为true用于等待服务器返回结果,再进行属性值的设置(进而view变化)
// 为false的话,先进行数据验证,再设置属性值(改变view, 拥有更好的用户体验),再提交数据,
// (这样的话等待服务器返回结果先还原attributes,进行属性值的set操作,包含验证)
if (attrs && !options.wait) {
if (!this.set(attrs, options)) return false;
} else {
if (!this._validate(attrs, options)) return false;
}
// Set temporary attributes if `{wait: true}`.
if (attrs && options.wait) {
this.attributes = _.extend({}, attributes, attrs);
}
// After a successful server-side save, the client is (optionally)
// updated with the server-side state.
if (options.parse === void 0) options.parse = true;
var model = this;
var success = options.success;
options.success = function (resp) {
// Ensure attributes are restored during synchronous saves.
model.attributes = attributes;
var serverAttrs = model.parse(resp, options);
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
return false;
}
if (success) success(model, resp, options);
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
if (method === 'patch') options.attrs = attrs;
xhr = this.sync(method, this, options);
// Restore attributes.
if (attrs && options.wait) this.attributes = attributes;
return xhr;
},
// Destroy this model on the server if it was already persisted.
// Optimistically removes the model from its collection, if it has one.
// If `wait: true` is passed, waits for the server to respond before removal.
destroy: function (options) {
options = options ? _.clone(options) : {};
var model = this;
var success = options.success;
var destroy = function () {
model.trigger('destroy', model, model.collection, options);
};
options.success = function (resp) {
if (options.wait || model.isNew()) destroy();
if (success) success(model, resp, options);
if (!model.isNew()) model.trigger('sync', model, resp, options);
};
if (this.isNew()) {
options.success();
return false;
}
wrapError(this, options);
var xhr = this.sync('delete', this, options);
if (!options.wait) destroy();
return xhr;
},
// Default URL for the model's representation on the server -- if you're
// using Backbone's restful methods, override this to change the endpoint
// that will be called.
url: function () {
var base =
_.result(this, 'urlRoot') ||
_.result(this.collection, 'url') ||
urlError();
if (this.isNew()) return base;
return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id);
},
// **parse** converts a response into the hash of attributes to be `set` on
// the model. The default implementation is just to pass the response along.
parse: function (resp, options) {
return resp;
},
// Create a new model with identical attributes to this one.
clone: function () {
return new this.constructor(this.attributes);
},
// A model is new if it has never been saved to the server, and lacks an id.
isNew: function () {
return !this.has(this.idAttribute);
},
// Check if the model is currently in a valid state.
isValid: function (options) {
return this._validate({}, _.extend(options || {}, { validate: true }));
},
// Run validation against the next complete set of model attributes,
// returning `true` if all is well. Otherwise, fire an `"invalid"` event.
_validate: function (attrs, options) {
if (!options.validate || !this.validate) return true;
attrs = _.extend({}, this.attributes, attrs);
var error = this.validationError = this.validate(attrs, options) || null;
if (!error) return true;
this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
return false;
}
});
// Underscore methods that we want to implement on the Model.
var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit', 'chain'];
// Mix in each Underscore method as a proxy to `Model#attributes`.
_.each(modelMethods, function (method) {
if (!_[method]) return;
Model.prototype[method] = function () {
var args = slice.call(arguments);
args.unshift(this.attributes);
return _[method].apply(_, args);
};
});
// Backbone.Collection
// -------------------
// If models tend to represent a single row of data, a Backbone Collection is
// more analogous to a table full of data ... or a small slice or page of that
// table, or a collection of rows that belong together for a particular reason
// -- all of the messages in this particular folder, all of the documents
// belonging to this particular author, and so on. Collections maintain
// indexes of their models, both in order, and for lookup by `id`.
// Create a new **Collection**, perhaps to contain a specific type of `model`.
// If a `comparator` is specified, the Collection will maintain
// its models in sort order, as they're added and removed.
var Collection = Backbone.Collection = function (models, options) {
options || (options = {});
if (options.model) this.model = options.model;
if (options.comparator !== void 0) this.comparator = options.comparator;
this._reset();
this.initialize.apply(this, arguments);
if (models) this.reset(models, _.extend({silent: true}, options));
};
// Default options for `Collection#set`.
var setOptions = {add: true, remove: true, merge: true};
var addOptions = {add: true, remove: false};
// Define the Collection's inheritable methods.
_.extend(Collection.prototype, Events, {
// The default model for a collection is just a **Backbone.Model**.
// This should be overridden in most cases.
model: Model,
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function () {
},
// The JSON representation of a Collection is an array of the
// models' attributes.
toJSON: function (options) {
return this.map(function (model) {
return model.toJSON(options);
});
},
// Proxy `Backbone.sync` by default.
sync: function () {
return Backbone.sync.apply(this, arguments);
},
// Add a model, or list of models to the set.
add: function (models, options) {
return this.set(models, _.extend({merge: false}, options, addOptions));
},
// Remove a model, or a list of models from the set.
remove: function (models, options) {
var singular = !_.isArray(models);
models = singular ? [models] : _.clone(models);
options || (options = {});
for (var i = 0, length = models.length; i < length; i++) {
var model = models[i] = this.get(models[i]);
if (!model) continue;
var id = this.modelId(model.attributes);
if (id != null) delete this._byId[id];
delete this._byId[model.cid];
var index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
this._removeReference(model, options);
}
return singular ? models[0] : models;
},
// 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) {
options = _.defaults({}, options, setOptions);
if (options.parse) models = this.parse(models, options);
var singular = !_.isArray(models);
models = singular ? (models ? [models] : []) : models.slice();
var id, model, attrs, existing, sort;
var at = options.at;
var sortable = this.comparator && (at == null) && options.sort !== false;
var sortAttr = _.isString(this.comparator) ? this.comparator : null;
var toAdd = [], toRemove = [], modelMap = {};
var add = options.add, merge = options.merge, remove = options.remove;
var order = !sortable && add && remove ? [] : false;
// Turn bare objects into model references, and prevent invalid models
// from being added.
for (var i = 0, length = models.length; i < length; i++) {
attrs = models[i];
// If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model.
if (existing = this.get(attrs)) {
if (remove) modelMap[existing.cid] = true;
if (merge && attrs !== existing) {
attrs = this._isModel(attrs) ? attrs.attributes : attrs;
if (options.parse) attrs = existing.parse(attrs, options);
existing.set(attrs, options);
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
}
models[i] = existing;
// If this is a new, valid model, push it to the `toAdd` list.
} else if (add) {
model = models[i] = this._prepareModel(attrs, options);
if (!model) continue;
toAdd.push(model);
this._addReference(model, options);
}
// Do not add multiple models with the same `id`.
model = existing || model;
if (!model) continue;
id = this.modelId(model.attributes);
if (order && (model.isNew() || !modelMap[id])) order.push(model);
modelMap[id] = true;
}
// Remove nonexistent models if appropriate.
if (remove) {
for (var i = 0, length = this.length; i < length; i++) {
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
}
if (toRemove.length) this.remove(toRemove, options);
}
// See if sorting is needed, update `length` and splice in new models.
if (toAdd.length || (order && order.length)) {
if (sortable) sort = true;
this.length += toAdd.length;
if (at != null) {
for (var i = 0, length = toAdd.length; i < length; i++) {
this.models.splice(at + i, 0, toAdd[i]);
}
} else {
if (order) this.models.length = 0;
var orderedModels = order || toAdd;
for (var i = 0, length = orderedModels.length; i < length; i++) {
this.models.push(orderedModels[i]);
}
}
}
// Silently sort the collection if appropriate.
if (sort) this.sort({silent: true});
// Unless silenced, it's time to fire all appropriate add/sort events.
if (!options.silent) {
for (var i = 0, length = toAdd.length; i < length; i++) {
(model = toAdd[i]).trigger('add', model, this, options);
}
if (sort || (order && order.length)) this.trigger('sort', this, options);
}
// Return the added (or merged) model (or models).
return singular ? models[0] : models;
},
// 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
// any granular `add` or `remove` events. Fires `reset` when finished.
// Useful for bulk operations and optimizations.
reset: function (models, options) {
options || (options = {});
for (var i = 0, length = this.models.length; i < length; i++) {
this._removeReference(this.models[i], options);
}
options.previousModels = this.models;
this._reset();
models = this.add(models, _.extend({silent: true}, options));
if (!options.silent) this.trigger('reset', this, options);
return models;
},
// Add a model to the end of the collection.
push: function (model, options) {
return this.add(model, _.extend({at: this.length}, options));
},
// Remove a model from the end of the collection.
pop: function (options) {
var model = this.at(this.length - 1);
this.remove(model, options);
return model;
},
// Add a model to the beginning of the collection.
unshift: function (model, options) {
return this.add(model, _.extend({at: 0}, options));
},
// Remove a model from the beginning of the collection.
shift: function (options) {
var model = this.at(0);
this.remove(model, options);
return model;
},
// Slice out a sub-array of models from the collection.
slice: function () {
return slice.apply(this.models, arguments);
},
// Get a model from the set by id.
get: function (obj) {
if (obj == null) return void 0;
var id = this.modelId(this._isModel(obj) ? obj.attributes : obj);
return this._byId[obj] || this._byId[id] || this._byId[obj.cid];
},
// Get the model at the given index.
at: function (index) {
return this.models[index];
},
// Return models with matching attributes. Useful for simple cases of
// `filter`.
where: function (attrs, first) {
if (_.isEmpty(attrs)) return first ? void 0 : [];
return this[first ? 'find' : 'filter'](function (model) {
for (var key in attrs) {
if (attrs[key] !== model.get(key)) return false;
}