開始在項目中大規模使用backbone,一路磕磕碰碰,邊作邊學習邊體會,有一些心得和體會,記錄在本文中。原文:Backbone使用總結javascript
Backbone.Events
就是事件實現的核心,它可讓對象擁有事件能力html
var Events = Backbone.Events = { .. }
對象經過listenTo
偵聽其餘對象,經過trigger
觸發事件。能夠脫離Backbone的MVC,在自定義的對象上使用事件java
var model = _.extend({},Backbone.Events); var view = _.extend({},Backbone.Events); view.listenTo(model,'custom_event',function(){ alert('catch the event') }); model.trigger('custom_event');
執行結果:jquery
Backbone的Model和View等核心類,都是繼承自Backbone.Events
的。例如Backbone.Model:ajax
var Events = Backbone.Events = { .. } var Model = Backbone.Model = function(attributes, options) { ... }; _.extend(Model.prototype, Events, { ... })
從原理上講,事件是這麼工做的:api
被偵聽的對象維護一個事件數組_event
,其餘對象在調用listenTo
時,會將事件名與回調維護到隊列中:數組
一個事件名能夠對應多個回調,對於被偵聽者而言,只知道回調的存在,並不知道具體是哪一個對象在偵聽它。當被偵聽者調用trigger(name)
時,會遍歷_event,選擇同名的事件,並將其下面全部的回調都執行一遍。服務器
須要額外注意的是,Backbone的listenTo
實現,除了使被偵聽者維護對偵聽者的引用外,還使偵聽者也維護了被偵聽者。這是爲了在恰當的時候,偵聽者能夠單方面中斷偵聽。所以,雖然是循環引用,可是使用Backbone的合適的方法能夠很好的維護,不會有問題,在後面的內存泄露部分將看到。app
另外,有時只但願事件在綁定後,當回調發生後,就接觸綁定。這在一些對公共模塊的引用時頗有用。listenToOnce
能夠作到這一點異步
backbone
默認實現了一套與RESTful風格的服務端同步模型的機制,這套機制不只能夠減輕開發人員的工做量,並且可使模型變得更爲健壯(在各類異常下仍能保持數據一致性)。不過,要真正發揮這個功效,一個與之匹配的服務端實現是很重要的。爲了說明問題,假設服務端有以下REST風格的接口:
/resources
獲取資源列表/resources
建立一個資源,返回資源的所有或部分字段/resources/{id}
獲取某個id的資源詳情,返回資源的所有或部分字段/resources/{id}
刪除某個資源/resources/{id}
更新某個資源的所有
字段,返回資源的所有或部分字段/resources/{id}
更新某個資源的部分
字段,返回資源的所有或部分字段backbone
會使用到上面這些HTTP方法的地方主要有如下幾個:
Model.save()
邏輯上,根據當前這個model的是否具備id
來判斷應該使用POST仍是PUT,若是model沒有id,表示是新的模型,將使用POST
,將模型的字段所有提交到/resources
;若是model具備id,表示是已經存在的模型,將使用PUT
,將模型的所有字段提交到/resources/{id}
。當傳入options
包含patch:true
的時候,save會產生PATCH
。Model.destroy()
會產生DELETE
,目標url爲/resources/{id}
,若是當前model不包含id時,不會與服務端同步,由於此時backbone認爲model在服務端尚不存在,不須要刪除Model.fetch()
會產生GET
,目標url爲/resources/{id}
,並將得到的屬性更新model。Collection.fetch()
會產生GET
,目標url爲/resources
,並對返回的數組中的每一個對象,自動實例化modelCollection.create()
實際將調用Model.save
options
參數存在於上面任何一個方法的參數列表中,經過options
能夠修改backbone和ajax請求的一些行爲,可使用的options包括:
wait
: 能夠指定是否等待服務端的返回結果再更新model。默認狀況下不等待url
: 能夠覆蓋掉backbone默認使用的url格式attrs
: 能夠指定保存到服務端的字段有哪些,配合options.patch
能夠產生PATCH
對模型進行部分更新patch
: 指定使用部分更新的REST接口data
: 會被直接傳遞給jquery的ajax中的data,可以覆蓋backbone全部的對上傳的數據控制的行爲其餘
: options中的任何參數都將直接傳遞給jquery的ajax,做爲其optionsbackbone經過Model的urlRoot
屬性或者是Collection
的url
屬性得知具體的服務端接口地址,以便發起ajax。在Model的url
默認實現中,Model除了會考察urlRoot
,第二選擇會是Model所在Collection的url
,全部有時只須要在Collection裏面書寫url
就能夠了。
Backbone會根據與服務端要進行什麼類型的操做,決定是否要添加id
在url
後面,如下代碼是Model的默認url
實現:
url: function () { var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); if (this.isNew()) return base; return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id); },
其中的正則式/([^\/])$/
是個很巧妙的處理,它解決了url
最後是否包含'/'
的不肯定性。
這個正則匹配的是行末的非
/
字符,這樣,像/resources
這樣的目標會匹配s
,而後replace
中使用分組編號$1
捕獲了s
,將s
替換爲s/
,這樣就自動加上了缺失的/
;而當/resources/
這樣目標卻沒法匹配到結果,也就不須要替換了。
在backbone中,即使一類的模型實例的確是在一個集合裏面,也並無強制要求使用集合類。可是使用集合有一些額外的好處,這些好處包括:
Model
屬於Collection
後,能夠繼承Collection的url
屬性。上面一節已經提到了
Collection
沿用了underscore
90%的集合和數組操做,使得集合操做極其方便:
// Underscore methods that we want to implement on the Collection. // 90% of the core usefulness of Backbone Collections is actually implemented // right here: var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', 'lastIndexOf', 'isEmpty', 'chain', 'sample'];
Backbone巧妙的使用下面的代碼將這些方法附加到Collection
中:
// Mix in each Underscore method as a proxy to `Collection#models`. _.each(methods, function (method) { Collection.prototype[method] = function () { var args = slice.call(arguments); //將參數數組轉化成真正的數組 args.unshift(this.models); //將Collection真正用來維護集合的數組,做爲第一個個參數 return _[method].apply(_, args); //使用apply調用underscore的方法 }; });
集合可以自動偵聽並轉發集合中的元素的事件,還有一些事件集合會作相應的特殊處理,這些事件包括:
destroy
偵聽到元素的destroy
事件後,會自動將元素從集合中移除,並引起remove
事件change:id
偵聽到元素的id屬性被change後,自動更新內部對model的引用關係利用Collection
的fetch
,能夠加載服務端數據集合,與此同時,能夠自動建立相關的Model實例,並調用構造方法
Collection
會根據Model
的idAttribute
指定的惟一鍵,來判斷元素是否重複,默認狀況下惟一鍵是id
,能夠重寫idAttribute
來覆蓋。當元素重複的時候,能夠選擇是丟棄重複元素,仍是合併兩種元素,默認是丟棄的
有時從REST接口獲得的數據並不能徹底知足界面的處理需求,能夠經過Model.parse
或者Collection.parse
方法,在實例化Backbone對象前,對數據進行預處理。大致上,Model.parse
用來對返回的單個對象進行屬性的處理,而Collection.parse
用來對返回的集合進行處理,一般是過濾掉沒必要要的數據。例如:
//只挑選type=1的book var Books = Backbone.Collection.extend({ parse:function(models,options){ return _.filter(models , function(model){ return model.type == 1; }) } }) //爲Book對象添加url屬性,以便渲染 var Book = Backbone.Model.extend({ parse: function(model,options){ return _.extend(model,{ url : '/books/' + model.id }); } })
經過Collection的fetch
,自動實例化的Model,其parse也會被調用。
Model能夠經過設置defaults
屬性來設置默認值,這頗有用。由於,不管是模型仍是集合,fetch數據都是異步的,而每每視圖的渲染確實極可能在數據到來前就進行了,若是沒有默認值的話,一些使用了模板引擎的視圖,在渲染的時候可能會出錯。例如underscore自帶的視圖引擎,因爲使用with(){}
語法,會由於對象缺少屬性而報錯。
Backbone的視圖對象十分簡答,對於開發者而言,僅僅關心一個el屬性便可。el屬性能夠經過五種途徑給出,優先級從高到低:
tagName
tagName
'div'
究竟如何選擇,取決於如下幾點:
tagName
通常對於自成體系的View有用,好比table中的某行tr,ul中的某個liblur
,對於這種View,只能選擇已經存在的html視圖類還有幾個屬性能夠導出,由外部初始化,它們是:
// List of view options to be merged as properties. var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
事件機制能夠很好的帶來代碼維護的便利,可是因爲事件綁定會使對象之間的引用變得複雜和錯亂,容易形成內存泄漏。下面的寫法就會形成內存泄漏:
var Task = Backbone.Model.extend({}) var TaskView = Backbone.View.extend({ tagName: 'tr', template: _.template('<td><%= id %></td><td><%= summary %></td><td><%= description %></td>'), initialize: function(){ this.listenTo(this.model,'change',this.render); }, render: function(){ this.$el.html( this.template( this.model.toJSON() ) ); return this; } }) var TaskCollection = Backbone.Collection.extend({ url: 'http://api.test.clippererm.com/api/testtasks', model: Task, comparator: 'summary' }) var TaskCollectionView = Backbone.View.extend({ initialize: function(){ this.listenTo(this.collection, 'add',this.addOne); this.listenTo(this.collection, 'reset',this.render); }, addOne: function(task){ var view = new TaskView({ model : task }); this.$el.append(view.render().$el); }, render: function(){ var _this = this; //簡單粗暴的將DOM清空 //在sort事件觸發的render調用時,以前實例化的TaskView對象會泄漏 this.$el.empty(); this.collection.each(function(model){ _this.addOne(model); }) return this; } })
使用下面的測試代碼,並結合Chrome的堆內存快照來證實:
var tasks = null; var tasklist = null; $(function () { // body... $('#start').click(function(){ tasks = new TaskCollection(); tasklist = new TaskCollectionView({ collection : tasks, el: '#tasklist' }) tasklist.render(); tasks.fetch(); }) $('#refresh').click(function(){ tasks.fetch({ reset : true }); }) $('#sort').click(function(){ //將偵聽sort放在這裏,避免第一次加載數據後的自動排序,觸發的sort事件,以致於混淆 tasklist.listenToOnce(tasks,'sort',tasklist.render); tasks.sort(); }) })
點擊開始,使用Chrome的'Profile'下的'Take Heap Snapshot'功能,查看當前堆內存狀況,使用child
類型過濾,能夠看到Backbone對象實例一共有10個(1+1+4+4):
之因此用child過濾,由於咱們的類繼承自Backbone的類型,而繼承使用了重寫原型的方法,Backbone在繼承時,使用的變量名爲
child
,最後,child
被返回出來了
點擊排序後,再次抓取快照,能夠看到實例個數變成了14個,這是由於,在render
過程當中,又建立了4個新的TaskView
,而以前的4個TaskView
並無釋放(之因此是4個是由於記錄的條數是4)
再次點擊排序,再次抓取快照,實例數又增長了4個,變成了18個!
那麼,爲何每次排序後,以前的TaskView
沒法釋放呢。由於TaskView的實例都會偵聽model,致使model對新建立的TaskView的實例存在引用,因此舊的TaskView沒法刪除,又建立了新的,致使內存不斷上漲。並且因爲引用存在於change
事件的回調隊列裏,model每次觸發change
都會通知舊的TaskView實例,致使執行不少無用的代碼。那麼如何改進呢?
修改TaskCollectionView:
var TaskCollectionView = Backbone.View.extend({ initialize: function(){ this.listenTo(this.collection, 'add',this.addOne); this.listenTo(this.collection, 'reset',this.render); //初始化一個view數組以跟蹤建立的view this.views =[] }, addOne: function(task){ var view = new TaskView({ model : task }); this.$el.append(view.render().$el); //將新建立的view保存起來 this.views.push(view); }, render: function(){ var _this = this; //遍歷views數組,並對每一個view調用Backbone的remove _.each(this.views,function(view){ view.remove().off(); }) //清空views數組,此時舊的view就變成沒有任何被引用的不可達對象了 //垃圾回收器會回收它們 this.views =[]; this.$el.empty(); this.collection.each(function(model){ _this.addOne(model); }) return this; } })
Backbone的View有一個remove
方法,這個方法除了刪除View所關聯的DOM對象,還會阻斷事件偵聽,它經過在listenTo方法時記錄下來的那些被偵聽對象(上文事件原理中提到),來使這些被偵聽的對象刪除對本身的引用。在remove
內部使用事件基類的stopListening
完成這個動做。
上面的代碼使用一個views數組來跟蹤新建立的TaskView
對象,並在render的時候,依次調用這些視圖對象的remove
,而後清空數組,這樣這些TaskView
對象就能獲得釋放。而且,除了調用remove
,還調用了off
,把視圖對象可能的被外部的偵聽也斷開。