前言
點保存時候不注意發出來了,有需要的朋友將就看吧,還在更新......
幾個月前學習了一下backbone,這段時間也用了下,感覺之前對backbone的學習很是基礎,前幾天有個園友問我如何將路由的#改為其他
我其實想說這個不能亂改,又怕不熟悉誤人子弟,所以今天我們來一起重新學習下他,看看會不會帶來不一樣的感覺
我在博客園nuysoft的博客看到了backbone的分析,可惜沒有寫完,不失為一個遺憾,希望作者堅持下去,水平高得貢獻出來喲(@nuysoft)
然后,網上backbone基礎用法的學習文章很多,感覺就nuysoft的深入,只不過多是點一下有點可惜,再次希望作者堅持下去......
Web應用越來越關注前端,現在一個服務器端可能要對付五個前端,前端的業務邏輯復雜,各種問題層出不窮,現實對javascript程序的重用性、健壯性提出了更高的要求
要求提高了,但是並不會給你更多的時間,反而為了搶占移動市場份額而拉快開發速度,現在的前端不可謂不難
PS:如果你的公司是互聯網公司且不重視前端的話,你可以來我們公司啊......
Backbone是一個基於MVC模式的架構,本身強依賴與underscore,所以上個星期我們初略的學習了下underscore,有了一個大概印象
非強制依賴於jquery/zepto,然后require是一個很好的基友,建議不要放過
backbone據我使用來看,有幾個優點:
① 模板引擎避免js中嵌入過多html代碼,這是一種結構數據分離的體現,但是他要歸功於underscore了
然后他的優點我用的時候就沒有了......
以上說法其實有點坑爹,我們為了減少backbone的size,所以對backbone做了刪除,最后只用到了其中的view(事件處理),控制器我們自己實現了
所以,我應該還未學習到backbone的精華,好了,前面扯多了,我們正式開始學習吧,這里附上之前的學習博客:
http://www.cnblogs.com/yexiaochai/p/3219402.html
http://www.cnblogs.com/yexiaochai/p/3221081.html
例子參考:http://www.ibm.com/developerworks/cn/web/wa-backbonejs/
簡單例子
我們今天首先做一個簡單的例子,然后通過例子去讀backbone的源碼,明天再整體進行學習,這個例子當然就是我們偉大的官方例子了......
第一步,頁面結構

1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="utf-8"> 5 <title>Backbone.js Todos</title> 6 <link rel="stylesheet" href="todos.css" /> 7 </head> 8 <body> 9 <div id="todoapp"> 10 <header> 11 <h1> 12 Todos</h1> 13 <input id="new-todo" type="text" placeholder="What needs to be done?"> 14 </header> 15 <section id="main"> 16 <input id="toggle-all" type="checkbox"> 17 <label for="toggle-all"> 18 Mark all as complete</label> 19 <ul id="todo-list"> 20 </ul> 21 </section> 22 <footer> 23 <a id="clear-completed">Clear completed</a> 24 <div id="todo-count"> 25 </div> 26 </footer> 27 </div> 28 <div id="instructions"> 29 Double-click to edit a todo. 30 </div> 31 <div id="credits"> 32 Created by 33 <br /> 34 <a href="http://jgn.me/">Jérôme Gravel-Niquet</a>. 35 <br /> 36 Rewritten by: <a href="http://addyosmani.github.com/todomvc">TodoMVC</a>. 37 </div> 38 <script src="../../test/vendor/json2.js"></script> 39 <script src="../../test/vendor/jquery.js"></script> 40 <script src="../../test/vendor/underscore.js"></script> 41 <script src="../../backbone.js"></script> 42 <script src="../backbone.localStorage.js"></script> 43 <script src="todos.js"></script> 44 <!-- Templates --> 45 <script type="text/template" id="item-template"> 46 <div class="view"> 47 <input class="toggle" type="checkbox" <%= done ? 'checked="checked"' : '' %> /> 48 <label><%- title %></label> 49 <a class="destroy"></a> 50 </div> 51 <input class="edit" type="text" value="<%- title %>" /> 52 </script> 53 <script type="text/template" id="stats-template"> 54 <% if (done) { %> 55 <a id="clear-completed">Clear <%= done %> completed <%= done == 1 ? 'item' : 'items' %></a> 56 <% } %> 57 <div class="todo-count"><b><%= remaining %></b> <%= remaining == 1 ? 'item' : 'items' %> left</div> 58 </script> 59 </body> 60 </html>
頁面結構比較簡單,其實就只有一個文本框,然后下面有一個用於顯示的列表,當然頁面中有我們用到的模板
第二步,定義model
然后我們需要定義我們備忘錄的model了
1 // Our basic **Todo** model has `title`, `order`, and `done` attributes. 2 var Todo = Backbone.Model.extend({ 3 4 // Default attributes for the todo item. 5 defaults: function () { 6 return { 7 title: "empty todo...", 8 order: Todos.nextOrder(), 9 done: false 10 }; 11 }, 12 // Toggle the `done` state of this todo item. 13 toggle: function () { 14 this.save({ done: !this.get("done") }); 15 } 16 });
這里需要注意他這種寫法,我們后面會詳細說明,這里先簡單來看看這個實例化的結果
我們看到一個Model實例化后有以上屬性,主要注意點是title與down,事實上我們可以使用model的get/set去操作這些屬性,model的主要工作其實就是維護他的屬性
model維護的屬性可能還會與服務器端發生通信,通信時會用到save方法,我們這里不予關注
1 // "name" attribute is set into the model 2 var team1 = new App.Models.Team({ 3 name : "name1" 4 }); 5 console.log(team1.get("name")); // prints "name1" 6 7 // "name" attribute is set with a new value 8 team1.set({ 9 name : "name2" 10 }); 11 console.log(team1.get("name")); //prints "name2"
這里需要注意的一點是,數據變化時候會引發Model的change方法,如果在change方法中,綁定對dom的操作,那么model變化頁面就會自動發生變化,這就是model這點小九九干的事情
代碼內部具體干什么的我們暫時不管,繼續往下看,有了model后就會有集合
第三步,集合
1 var TodoList = Backbone.Collection.extend({ 2 // Reference to this collection's model. 3 model: Todo, 4 // Save all of the todo items under the `"todos-backbone"` namespace. 5 localStorage: new Backbone.LocalStorage("todos-backbone"), 6 // Filter down the list of all todo items that are finished. 7 done: function () { 8 return this.where({ done: true }); 9 }, 10 // Filter down the list to only todo items that are still not finished. 11 remaining: function () { 12 return this.without.apply(this, this.done()); 13 }, 14 // We keep the Todos in sequential order, despite being saved by unordered 15 // GUID in the database. This generates the next order number for new items. 16 nextOrder: function () { 17 if (!this.length) return 1; 18 return this.last().get('order') + 1; 19 }, 20 // Todos are sorted by their original insertion order. 21 comparator: 'order' 22 });
這里對集合與之前定義的Model做了一個映射關系,他這個集合有何作用已經如何發生變化的我們后面詳細說明
這里我只能說,這個集合時保存Model的列表,並在內部定義了一些操作model的方法
集合與model息息相關,事實上每個model內部都有一個collection的映射對象,一旦發生映射,那么model變化collection內部也會發生變化,這里的細節我們后面點說
第四步,TodoView

1 var TodoView = Backbone.View.extend({ 2 3 //... is a list tag. 4 tagName: "li", 5 6 // Cache the template function for a single item. 7 template: _.template($('#item-template').html()), 8 9 // The DOM events specific to an item. 10 events: { 11 "click .toggle": "toggleDone", 12 "dblclick .view": "edit", 13 "click a.destroy": "clear", 14 "keypress .edit": "updateOnEnter", 15 "blur .edit": "close" 16 }, 17 18 // The TodoView listens for changes to its model, re-rendering. Since there's 19 // a one-to-one correspondence between a **Todo** and a **TodoView** in this 20 // app, we set a direct reference on the model for convenience. 21 initialize: function () { 22 this.listenTo(this.model, 'change', this.render); 23 this.listenTo(this.model, 'destroy', this.remove); 24 }, 25 26 // Re-render the titles of the todo item. 27 render: function () { 28 this.$el.html(this.template(this.model.toJSON())); 29 this.$el.toggleClass('done', this.model.get('done')); 30 this.input = this.$('.edit'); 31 return this; 32 }, 33 34 // Toggle the `"done"` state of the model. 35 toggleDone: function () { 36 this.model.toggle(); 37 }, 38 39 // Switch this view into `"editing"` mode, displaying the input field. 40 edit: function () { 41 this.$el.addClass("editing"); 42 this.input.focus(); 43 }, 44 45 // Close the `"editing"` mode, saving changes to the todo. 46 close: function () { 47 var value = this.input.val(); 48 if (!value) { 49 this.clear(); 50 } else { 51 this.model.save({ title: value }); 52 this.$el.removeClass("editing"); 53 } 54 }, 55 56 // If you hit `enter`, we're through editing the item. 57 updateOnEnter: function (e) { 58 if (e.keyCode == 13) this.close(); 59 }, 60 61 // Remove the item, destroy the model. 62 clear: function () { 63 this.model.destroy(); 64 } 65 66 });
熟悉backbone的朋友一定對這段代碼尤其熟悉(因為我們只用到了view,model與control全部自己寫的,所以我最熟悉的就是這個......)
上面的代碼會形成一個view,view實例化時會執行initialize中的方法,需要顯示則需要執行render方法(重寫)
render結束后頁面的交互全部放到了events里面,各位既然用到了backbone,就不要自己隨意以on的形式綁定事件了
在view可以為el指定dom結構,新建的view形成的dom就會往里面裝
總而言之,View的重點是模板引擎與事件綁定,這里的view不是入口方法,我們看下一個view
第五步,入口AppView

1 var AppView = Backbone.View.extend({ 2 3 // Instead of generating a new element, bind to the existing skeleton of 4 // the App already present in the HTML. 5 el: $("#todoapp"), 6 7 // Our template for the line of statistics at the bottom of the app. 8 statsTemplate: _.template($('#stats-template').html()), 9 10 // Delegated events for creating new items, and clearing completed ones. 11 events: { 12 "keypress #new-todo": "createOnEnter", 13 "click #clear-completed": "clearCompleted", 14 "click #toggle-all": "toggleAllComplete" 15 }, 16 17 // At initialization we bind to the relevant events on the `Todos` 18 // collection, when items are added or changed. Kick things off by 19 // loading any preexisting todos that might be saved in *localStorage*. 20 initialize: function () { 21 22 this.input = this.$("#new-todo"); 23 this.allCheckbox = this.$("#toggle-all")[0]; 24 25 this.listenTo(Todos, 'add', this.addOne); 26 this.listenTo(Todos, 'reset', this.addAll); 27 this.listenTo(Todos, 'all', this.render); 28 29 this.footer = this.$('footer'); 30 this.main = $('#main'); 31 32 Todos.fetch(); 33 }, 34 35 // Re-rendering the App just means refreshing the statistics -- the rest 36 // of the app doesn't change. 37 render: function () { 38 var done = Todos.done().length; 39 var remaining = Todos.remaining().length; 40 41 if (Todos.length) { 42 this.main.show(); 43 this.footer.show(); 44 this.footer.html(this.statsTemplate({ done: done, remaining: remaining })); 45 } else { 46 this.main.hide(); 47 this.footer.hide(); 48 } 49 50 this.allCheckbox.checked = !remaining; 51 }, 52 53 // Add a single todo item to the list by creating a view for it, and 54 // appending its element to the `<ul>`. 55 addOne: function (todo) { 56 var view = new TodoView({ model: todo }); 57 this.$("#todo-list").append(view.render().el); 58 }, 59 60 // Add all items in the **Todos** collection at once. 61 addAll: function () { 62 Todos.each(this.addOne, this); 63 }, 64 65 // If you hit return in the main input field, create new **Todo** model, 66 // persisting it to *localStorage*. 67 createOnEnter: function (e) { 68 if (e.keyCode != 13) return; 69 if (!this.input.val()) return; 70 71 Todos.create({ title: this.input.val() }); 72 this.input.val(''); 73 }, 74 75 // Clear all done todo items, destroying their models. 76 clearCompleted: function () { 77 _.invoke(Todos.done(), 'destroy'); 78 return false; 79 }, 80 81 toggleAllComplete: function () { 82 var done = this.allCheckbox.checked; 83 Todos.each(function (todo) { todo.save({ 'done': done }); }); 84 } 85 86 });
很遺憾的是,這個代碼沒有用到路由相關的知識,至此就結束了,因為路由相關的知識是單頁應用的一大重點,但是對我們學習來說夠了
這里定義了AppView后便實例化了,我們這里來詳細讀讀這個入口函數
① 初始化操作
1 initialize: function () { 2 3 this.input = this.$("#new-todo"); 4 this.allCheckbox = this.$("#toggle-all")[0]; 5 6 this.listenTo(Todos, 'add', this.addOne); 7 this.listenTo(Todos, 'reset', this.addAll); 8 this.listenTo(Todos, 'all', this.render); 9 10 this.footer = this.$('footer'); 11 this.main = $('#main'); 12 13 Todos.fetch(); 14 },
首先這里做了初始化操作,在這里,我們可以開開心心定義一些后面會用到的dom,這里有一個比較有意思的方法:
this.$();//其實是this.root.find(),這個可以保證你找到正確的元素
在單頁應用中,id的唯一性收到了吹殘,所以獲得元素的方式得到了便會,以上是一種,不明白以上方法的同學喜歡用:
this.$el.find();//$el事實上就是根元素
fetch方法用於初始化集合數據,意思是Todos.fetch();執行結束后,集合就被model給填充了(這里在localstorage中讀取了數據)
Todos填充數據后,便會調用本身的render方法將數據以dom形式呈現在我們眼前
1 this.listenTo(Todos, 'add', this.addOne); 2 this.listenTo(Todos, 'reset', this.addAll); 3 this.listenTo(Todos, 'all', this.render);
這里的的listenTo事實上是一種自定義事件的寫法,fetch時候觸發了其中的all事件,所以執行了render方法渲染頁面
這里頁面初始化完成了,當然,一開始我們列表其實是空的
② 增加操作
增加操作簡單說一下即可,這里為new-todo(文本框)綁定了keypress事件,事件會觸發createOnEnter函數
1 createOnEnter: function (e) { 2 if (e.keyCode != 13) return; 3 if (!this.input.val()) return; 4 5 Todos.create({ title: this.input.val() }); 6 this.input.val(''); 7 },
這里愉快的使用集合create方法創建了一個model,當然他觸發自己綁定的add事件,於是執行了
1 addOne: function (todo) { 2 var view = new TodoView({ model: todo }); 3 this.$("#todo-list").append(view.render().el); 4 },
這個操作自然不是省油的燈,他本身也是綁定了change事件的,於是高高興興在頁面中新增了一條數據:
1 initialize: function () { 2 this.listenTo(this.model, 'change', this.render); 3 this.listenTo(this.model, 'destroy', this.remove); 4 }, 5 6 // Re-render the titles of the todo item. 7 render: function () { 8 this.$el.html(this.template(this.model.toJSON())); 9 this.$el.toggleClass('done', this.model.get('done')); 10 this.input = this.$('.edit'); 11 return this; 12 },
至此整個邏輯基本結束,其它方面我這里暫時不涉及,這個模式比較好的是,我們就只需要關注model數據變化即可,頁面上顯示的東西就不管了
PS:說是不管,其實一開始就管完了,只是需要觸發事件即可
至此,我們基本例子分析結束,我們下面帶着這個例子去學習下backbone的源碼,這里又不得不嘆息,這里未使用路由(控制器)功能
實現繼承-extend
我們無論定義Model還是View時候都是這樣干的:
var Todo = Backbone.Model.extend({}) var TodoList = Backbone.Collection.extend({}) var TodoView = Backbone.View.extend({})
其實這個extend着實使人疑惑,因為在underscore學習時候,我們知道他是這樣的:
1 // 將一個或多個對象的屬性(包含原型鏈中的屬性), 復制到obj對象, 如果存在同名屬性則覆蓋 2 _.extend = function(obj) { 3 // each循環參數中的一個或多個對象 4 each(slice.call(arguments, 1), function(source) { 5 // 將對象中的全部屬性復制或覆蓋到obj對象 6 for(var prop in source) { 7 obj[prop] = source[prop]; 8 } 9 }); 10 return obj; 11 };
這個東西和上述實現差了一大截,於是進入源碼一看,事實上我們看到的應該是inherits方法,但是1.0滅掉了inherits方法,只剩下了extend了
1 var extend = function(protoProps, staticProps) { 2 var parent = this; 3 var child; 4 5 // The constructor function for the new subclass is either defined by you 6 // (the "constructor" property in your `extend` definition), or defaulted 7 // by us to simply call the parent's constructor. 8 if (protoProps && _.has(protoProps, 'constructor')) { 9 child = protoProps.constructor; 10 } else { 11 child = function(){ return parent.apply(this, arguments); }; 12 } 13 14 // Add static properties to the constructor function, if supplied. 15 _.extend(child, parent, staticProps); 16 17 // Set the prototype chain to inherit from `parent`, without calling 18 // `parent`'s constructor function. 19 var Surrogate = function(){ this.constructor = child; }; 20 Surrogate.prototype = parent.prototype; 21 child.prototype = new Surrogate; 22 23 // Add prototype properties (instance properties) to the subclass, 24 // if supplied. 25 if (protoProps) _.extend(child.prototype, protoProps); 26 27 // Set a convenience property in case the parent's prototype is needed 28 // later. 29 child.__super__ = parent.prototype; 30 31 return child; 32 }; 33 34 // Set up inheritance for the model, collection, router, view and history. 35 Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
這個家伙又是如何實現繼承的,我們這里詳細來看看,首先我們將這段代碼分離出來:

1 var hasOwnProperty = Object.prototype.hasOwnProperty; 2 var slice = Array.prototype.slice; 3 var nativeForEach = Array.prototype.forEach; 4 var _ = {}; 5 6 var each = _.each = _.forEach = function (obj, iterator, context) { 7 if (obj == null) 8 return; 9 if (nativeForEach && obj.forEach === nativeForEach) { 10 obj.forEach(iterator, context); 11 } else if (obj.length === +obj.length) { 12 for (var i = 0, l = obj.length; i < l; i++) { 13 if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) 14 return; 15 } 16 } else { 17 for (var key in obj) { 18 if (_.has(obj, key)) { 19 if (iterator.call(context, obj[key], key, obj) === breaker) 20 return; 21 } 22 } 23 } 24 } 25 26 _.has = function (obj, key) { 27 return hasOwnProperty.call(obj, key); 28 }; 29 30 _.extend = function (obj) { 31 each(slice.call(arguments, 1), function (source) { 32 for (var prop in source) { 33 obj[prop] = source[prop]; 34 } 35 }); 36 return obj; 37 }; 38 39 var extend = function (protoProps, staticProps) { 40 var parent = this; 41 var child; 42 if (protoProps && _.has(protoProps, 'constructor')) { 43 child = protoProps.constructor; 44 } else { 45 child = function () { return parent.apply(this, arguments); }; 46 } 47 _.extend(child, parent, staticProps); 48 var Surrogate = function () { this.constructor = child; }; 49 Surrogate.prototype = parent.prototype; 50 child.prototype = new Surrogate; 51 if (protoProps) _.extend(child.prototype, protoProps); 52 child.__super__ = parent.prototype; 53 return child; 54 }; 55 56 57 var P = function () { 58 this.a = 1; 59 this.b = 2; 60 }; 61 62 P.prototype.aa = function () { 63 alert(this.a); 64 } 65 66 P.extend = extend; 67 68 var C = P.extend({ 69 c: 3, 70 cc: function () { 71 alert(this.c); 72 } 73 }); 74 75 var s = '';
1 var c = new C; 2 c.cc();//3 3 c.aa();//1
backbone實現的繼承以最基礎的繼承,因為他只支持一層的繼承,要再多可能這個做法就不好使了,原來inherits其實可以多層繼承的......
反正,我們來讀一讀extend代碼
① parent=this
這個代碼其實是保留當前函數,比如View或者Model,后面的child會繼承他的方法
② protoProps
protoProps實際上是原型方法,如果具有constactor屬性,變直接繼承之,否則重新定義一個函數,函數初始化(構造函數)時會調用parent的構造函數
PS:而這里parent會執行一些初始化操作,然后調用this.initialize.apply(this, arguments); 所以我們代碼中的initialize一定會執行
③ 復制靜態屬性
然后使用將parent靜態屬性一一收入成自己的
_.extend(child, parent, staticProps);
④ 經典繼承法
1 var Surrogate = function () { this.constructor = child; }; 2 Surrogate.prototype = parent.prototype; 3 child.prototype = new Surrogate; 4 if (protoProps) _.extend(child.prototype, protoProps); 5 child.__super__ = parent.prototype;
然后使用此經典的方法實現繼承,最終返回給我們child,注意其中的_.extend(child.prototype, protoProps);我們增加的屬性全部給加到了原型鏈上了
至此,backbone的基本繼承,我們閱讀完畢,現在看到以下代碼要清除發生了什么事才行
var TodoView = Backbone.View.extend({})
下面我們來看看backbone的事件機制
事件機制-Events
backbone事件一塊就只放出了三個接口:bind、unbind、trigger
Events 是一個可以被mix到任意對象的模塊,它擁有讓對象綁定和觸發自定義事件的能力。
事件在被綁定之前是不需要事先聲明的,還可以攜帶參數。我們通過一個例子來看:
var object = {}; _.extend(object, Backbone.Events); object.bind("alert", function(msg) { alert("Triggered " + msg); }); object.trigger("alert", "www.csser.com");
bind用於注冊事件,unbind注銷事件,trigger觸發事件,但是內部的事件一塊遠不止如此
PS:老夫突然發現我看的中文api是0.5.3的版本!!!fuck me!!! 1.1放出了這么多的接口,額......
首先我們來看一看他Events的源碼:
1 var Events = Backbone.Events = { 2 3 // Bind an event to a `callback` function. Passing `"all"` will bind 4 // the callback to all events fired. 5 on: function(name, callback, context) { 6 if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 7 this._events || (this._events = {}); 8 var events = this._events[name] || (this._events[name] = []); 9 events.push({callback: callback, context: context, ctx: context || this}); 10 return this; 11 }, 12 13 // Bind an event to only be triggered a single time. After the first time 14 // the callback is invoked, it will be removed. 15 once: function(name, callback, context) { 16 if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; 17 var self = this; 18 var once = _.once(function() { 19 self.off(name, once); 20 callback.apply(this, arguments); 21 }); 22 once._callback = callback; 23 return this.on(name, once, context); 24 }, 25 26 // Remove one or many callbacks. If `context` is null, removes all 27 // callbacks with that function. If `callback` is null, removes all 28 // callbacks for the event. If `name` is null, removes all bound 29 // callbacks for all events. 30 off: function(name, callback, context) { 31 var retain, ev, events, names, i, l, j, k; 32 if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; 33 if (!name && !callback && !context) { 34 this._events = {}; 35 return this; 36 } 37 38 names = name ? [name] : _.keys(this._events); 39 for (i = 0, l = names.length; i < l; i++) { 40 name = names[i]; 41 if (events = this._events[name]) { 42 this._events[name] = retain = []; 43 if (callback || context) { 44 for (j = 0, k = events.length; j < k; j++) { 45 ev = events[j]; 46 if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || 47 (context && context !== ev.context)) { 48 retain.push(ev); 49 } 50 } 51 } 52 if (!retain.length) delete this._events[name]; 53 } 54 } 55 56 return this; 57 }, 58 59 // Trigger one or many events, firing all bound callbacks. Callbacks are 60 // passed the same arguments as `trigger` is, apart from the event name 61 // (unless you're listening on `"all"`, which will cause your callback to 62 // receive the true name of the event as the first argument). 63 trigger: function(name) { 64 if (!this._events) return this; 65 var args = slice.call(arguments, 1); 66 if (!eventsApi(this, 'trigger', name, args)) return this; 67 var events = this._events[name]; 68 var allEvents = this._events.all; 69 if (events) triggerEvents(events, args); 70 if (allEvents) triggerEvents(allEvents, arguments); 71 return this; 72 }, 73 74 // Tell this object to stop listening to either specific events ... or 75 // to every object it's currently listening to. 76 stopListening: function(obj, name, callback) { 77 var listeners = this._listeners; 78 if (!listeners) return this; 79 var deleteListener = !name && !callback; 80 if (typeof name === 'object') callback = this; 81 if (obj) (listeners = {})[obj._listenerId] = obj; 82 for (var id in listeners) { 83 listeners[id].off(name, callback, this); 84 if (deleteListener) delete this._listeners[id]; 85 } 86 return this; 87 } 88 89 };
這里統一使用了eventApi這個函數:
1 // Regular expression used to split event strings. 2 var eventSplitter = /\s+/; 3 4 // Implement fancy features of the Events API such as multiple event 5 // names `"change blur"` and jQuery-style event maps `{change: action}` 6 // in terms of the existing API. 7 var eventsApi = function(obj, action, name, rest) { 8 if (!name) return true; 9 10 // Handle event maps. 11 if (typeof name === 'object') { 12 for (var key in name) { 13 obj[action].apply(obj, [key, name[key]].concat(rest)); 14 } 15 return false; 16 } 17 18 // Handle space separated event names. 19 if (eventSplitter.test(name)) { 20 var names = name.split(eventSplitter); 21 for (var i = 0, l = names.length; i < l; i++) { 22 obj[action].apply(obj, [names[i]].concat(rest)); 23 } 24 return false; 25 } 26 27 return true; 28 };
① 第一個參數為一個對象,其實指向的是調用者,因為Events對象都是被作為 繼承/擴展 者使用
② 第二個參數為你的具體操作(on/off/trigger)
③ name可算是這個對象注冊的這個事件的唯一標識了,注冊事件后后面會用他來讀取
④ 最后是傳入的回調函數,並且帶有作用域
而上述調用點又在其它地方,我們這里將上述代碼連起來:
① 首先一個對象繼承了Events對象
var obj = {}; _.extend(obj, Backbone.Events)
② 其次我們為他注冊一個alert事件
obj.on('alert', function (msg) { alert(msg); });
此時會調用由Events繼承而來的方法on,並且傳入兩個參數:alert與回調函數,而后會調用eventApi(私有方法)處理這個event對象
PS:此處傳入的那么不是對象也沒有任何復雜應用所以直接返回true了,像那么包含“:”,或者包含空格就會做特殊處理,我們這里暫時不管
③ 定義對象的events屬性,該屬性用於存儲該對象保存的所有事件
1 on: function(name, callback, context) { 2 if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 3 this._events || (this._events = {}); 4 var events = this._events[name] || (this._events[name] = []); 5 events.push({callback: callback, context: context, ctx: context || this}); 6 return this; 7 },
處理結束后,這里便會多出一個對象了:
③ 觸發事件,觸發事件相對比較簡單,可以選擇傳入參數
obj.trigger("alert", "an event");
觸發事件,當然是調用的trigger方法:
1 trigger: function(name) { 2 if (!this._events) return this; 3 var args = slice.call(arguments, 1); 4 if (!eventsApi(this, 'trigger', name, args)) return this; 5 var events = this._events[name]; 6 var allEvents = this._events.all; 7 if (events) triggerEvents(events, args); 8 if (allEvents) triggerEvents(allEvents, arguments); 9 return this; 10 },
這里會通過name在events屬性中獲取當前對象,調用triggerEvents局部函數調用之,這里有兩點需要注意:
1 這里的args是除,name以外傳入的參數
2 這里會觸發name為all的事件,無論如何都會觸發,各位這里要回想起來前面集合的listenerTo方法哦
1 var triggerEvents = function(events, args) { 2 var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; 3 switch (args.length) { 4 case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; 5 case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; 6 case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; 7 case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; 8 default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); 9 } 10 };
然后這里做了下簡單的處理,高高興興將我們定義的事件執行了,於是backbone事件機制的第一段講解也結束了,較詳細的講解后面點再來
在此我們可以看到,backbone內部的事件機制,其實與javascript事件綁定那塊沒什么聯系,只不過是自己內部的實現而已,這里最后補充一點:
1 var listenMethods = { listenTo: 'on', listenToOnce: 'once' }; 2 3 // Inversion-of-control versions of `on` and `once`. Tell *this* object to 4 // listen to an event in another object ... keeping track of what it's 5 // listening to. 6 _.each(listenMethods, function (implementation, method) { 7 Events[method] = function (obj, name, callback) { 8 var listeners = this._listeners || (this._listeners = {}); 9 var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); 10 listeners[id] = obj; 11 if (typeof name === 'object') callback = this; 12 obj[implementation](name, callback, this); 13 return this; 14 }; 15 });
頁面上調用的listenTo其實就是on方法
模型-Model
構造函數
Model在服務器端來說很是關鍵,記得前幾年老夫還在搞.net最先干的事情就是建造實體,不知道現在怎么樣了......
首先看看其繼承源碼:
1 var Model = Backbone.Model = function (attributes, options) { 2 var defaults; 3 var attrs = attributes || {}; 4 options || (options = {}); 5 this.cid = _.uniqueId('c'); 6 this.attributes = {}; 7 _.extend(this, _.pick(options, modelOptions)); 8 if (options.parse) attrs = this.parse(attrs, options) || {}; 9 if (defaults = _.result(this, 'defaults')) { 10 attrs = _.defaults({}, attrs, defaults); 11 } 12 this.set(attrs, options); 13 this.changed = {}; 14 this.initialize.apply(this, arguments); 15 };
構造函數中本身沒有干太多事情:
① 首先為該model定義了唯一的cid(其中的uniqueId方法內部維護着一個id,這個閉包知識點,各位自己去看吧)
② 初始化model默認的屬性,比如collection就是必須擁有的,如果定義了的話就直接搞給對象
③ parse是為了兼容不是json數據時候需要做的處理,我們這里直接忽略不要json的場景
④ 獲取defaults對象(如果是函數需要返回對象)
⑤ 調用原型鏈中的set方法,將默認的屬性搞到對象中去,set干的事情比較多,我們后面點說
反正他產生的結果就是對象默認會多一些屬性值
然后開始調用underscore的exentd擴展對象的原型鏈了(尼瑪,backbone確實強依賴underscore啊,壓根搞不掉)
擴展原型鏈
下面開始擴展Model原型鏈了,其實這樣讀下來,backbone的代碼是很有調理的,很好讀,我們這里撿幾個重要的說(我不知道哪個重要只能挑我知道的)
① changed
changed屬性記錄了每次調用set方法時, 被改變數據的key集合
② validationError
set model 時候會執行validate方法,如果驗證失敗便會將結果返回該變量
③ idAttribute
每個模型的唯一標識屬性(默認為"id", 通過修改idAttribute可自定義id屬性名)
如果在設置數據時包含了id屬性, 則id將會覆蓋模型的id,id用於在Collection集合中查找和標識模型, 與后台接口通信時也會以id作為一條記錄的標識
var Meal = Backbone.Model.extend({ idAttribute: "_id" }); var cake = new Meal({ _id: 1, name: "Cake" }); alert("Cake id: " + cake.id);
這里就將標識符搞到了_id屬性上,但是一般不建議這么干,真心不太好......
④ initialize
這個方法比較關鍵,本身沒有意義,用於子對象復寫,會在實例化時候執行
⑤ get
返回相關屬性的值
⑥ set(key, value, options)
這個方法很關鍵,我們這里來詳細說下
1 set: function (key, val, options) { 2 var attr, attrs, unset, changes, silent, changing, prev, current; 3 if (key == null) return this; 4 5 // Handle both `"key", value` and `{key: value}` -style arguments. 6 if (typeof key === 'object') { 7 attrs = key; 8 options = val; 9 } else { 10 (attrs = {})[key] = val; 11 } 12 13 options || (options = {}); 14 15 // Run validation. 16 if (!this._validate(attrs, options)) return false; 17 18 // Extract attributes and options. 19 unset = options.unset; 20 silent = options.silent; 21 changes = []; 22 changing = this._changing; 23 this._changing = true; 24 25 if (!changing) { 26 this._previousAttributes = _.clone(this.attributes); 27 this.changed = {}; 28 } 29 current = this.attributes, prev = this._previousAttributes; 30 31 // Check for changes of `id`. 32 if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 33 34 // For each `set` attribute, update or delete the current value. 35 for (attr in attrs) { 36 val = attrs[attr]; 37 if (!_.isEqual(current[attr], val)) changes.push(attr); 38 if (!_.isEqual(prev[attr], val)) { 39 this.changed[attr] = val; 40 } else { 41 delete this.changed[attr]; 42 } 43 unset ? delete current[attr] : current[attr] = val; 44 } 45 46 // Trigger all relevant attribute changes. 47 if (!silent) { 48 if (changes.length) this._pending = true; 49 for (var i = 0, l = changes.length; i < l; i++) { 50 this.trigger('change:' + changes[i], this, current[changes[i]], options); 51 } 52 } 53 54 // You might be wondering why there's a `while` loop here. Changes can 55 // be recursively nested within `"change"` events. 56 if (changing) return this; 57 if (!silent) { 58 while (this._pending) { 59 this._pending = false; 60 this.trigger('change', this, options); 61 } 62 } 63 this._pending = false; 64 this._changing = false; 65 return this; 66 },
model.set(attributes, [options])
這里第一個參數可以為對象或者字符串,最簡單的情況當然是:
var m = new Model(); m.set('name', '葉小釵');
這樣會開開心心執行個對象就結束,當然也可以這樣:
m.set({'name': '葉小釵'});
於是,第二個參數的意義就不大了......
{silent: true}的情況下不會觸發change事件
1 首先,做了簡單的參數檢查,將對象放入了attrs變量
2 其次,執行了一次驗證操作,如果驗證不成立,這里會直接退出去
3 然后,操作傳入的options(必須是對象)
這里我有點不太理解:
如果options設置了unset屬性,則將attrs的所有值設置為undefined
如果options沒有指定silent屬性, 則直接設置changes屬性中當前數據為已改變狀態
4 進行操作前_previousAttributes會保存改變前的屬性值,這里有個changing值得注意,他用於檢測一次set觸發時執行才change方法是否結束,沒有結束的話便不能執行
5 遍歷時候如果要設置的屬性與當前值不等,則將該key值壓入changes數組,如果與之前的不等,則在changed對象中賦值(changed記錄了每次set時候改變的鍵值)
如果被相等的話,就將他移除changed對象,如果設置了unset屬性,則需要刪除當前屬性否則就賦值
PS:尼瑪,這里在干什么,我沒搞明白,先放放吧
6 下面如果沒有設置silent,的話會將上面設置的changes中的數據提出來,並觸發相關事件(比如觸發changename事件,但是我們並未定義)
然后觸發整個model的change事件,這個我們應該會綁定,最后做一點結尾處理就跳出來了,我的結論就我沒太看懂......后面再看看吧
⑦ unset
刪除屬性
return this.set(attr, void 0, _.extend({}, options, { unset: true }));
看着unset,我突然好像知道set下面干了寫什么事情了......原來他刪除與添加都寫到了一起了
⑧ fetch
據說是由服務器端獲取數據,然后使用set方法初始化model數據,
1 fetch: function (options) { 2 options = options ? _.clone(options) : {}; 3 if (options.parse === void 0) options.parse = true; 4 var model = this; 5 var success = options.success; 6 options.success = function (resp) { 7 if (!model.set(model.parse(resp, options), options)) return false; 8 if (success) success(model, resp, options); 9 model.trigger('sync', model, resp, options); 10 }; 11 wrapError(this, options); 12 return this.sync('read', this, options); 13 },
從服務器重置模型狀態。這對模型尚未填充數據,或者服務器端已有最新狀態的情況很有用處。 如果服務器端狀態與當前屬性不同,則觸發 "change" 事件。
選項的散列表參數接受 success 和 error 回調函數, 回調函數中可以傳入 (model,response) 作為參數。
這里具體使用了sync事件由服務器端獲取數據,這個sync實際上封裝了ajax操作,會使用model設置的url,鍵值為id,所以此處我們就不關注了
⑨ _validate
1 _validate: function (attrs, options) { 2 if (!options.validate || !this.validate) return true; 3 attrs = _.extend({}, this.attributes, attrs); 4 var error = this.validationError = this.validate(attrs, options) || null; 5 if (!error) return true; 6 this.trigger('invalid', this, error, _.extend(options || {}, { validationError: error })); 7 return false; 8 }
用於驗證屬性的函數,如果為屬性定義了validate驗證方法,這里就會被調用,如果調用失敗還會觸發一個事件,Model一塊我們暫時就結束了,詳細的下面點分析
PS:肚子有點餓,戰斗力不行了
結語
下次我們繼續學習集合