摘要:個人認為,讀懂老牌框架的源代碼比會用流行框架的要有用的多。另外,源代碼中所有的以開頭的方法,可以認為是私有方法,是沒有必要直接使用的,也不建議用戶覆蓋。
backbone是我兩年多前入門前端的時候接觸到的第一個框架,當初被backbone的強大功能所吸引(當然的確比裸寫js要好得多),雖然現在backbone并不算最主流的前端框架了,但是,它里面大量設計模式的靈活運用,以及令人贊嘆的處理技巧,還是非常值得學習。個人認為,讀懂老牌框架的源代碼比會用流行框架的API要有用的多。
另外,backbone的源代碼最近也改了許多(特別是針對ES6),所以有些老舊的分析,可能會和現在的源代碼有些出入。
所以我寫這一篇分析backbone的文章,供自己和大家一起學習,本文適合使用過backbone的朋友,筆者水平有限,而內容又實有點多,難免會出差錯,歡迎大家在GitHub上指正
接下來,我們將通過一篇文章解析backbone,我們是按照源碼的順序來講解的,這有利于大家邊看源代碼邊解讀,另外,我給源代碼加了全部的中文注釋和批注,請見這里,強烈建議大家邊看源碼邊看解析,并且遇到我給出外鏈的地方,最好把外鏈的內容也看看(如果能夠給大家幫助,歡迎給star鼓勵~)
當然,這篇文章很長[為了避免文章有上沒下,我還是整合到一篇文章中了]。
backbone宏觀解讀backbone是很早期將MVC的思想帶入前端的框架,現在MVC以及后來的MVVM這么火可以在一定程度上歸功于backbone。關于前端MVC,我在自己的這篇文章中結合阮一峰老師的圖示簡單分析過,簡單來講就是Model層控制數據,View層通過發布訂閱(在backbone中)來處理和用戶的交互,Controller是控制器,在這里主要是指backbone的路由功能。這樣的設計非常直接清晰,有利于前端工程化。
backbone中主要實現了Model、Collection、View、Router、History幾大功能,前四種我們用的比較多,另外backbone基于發布-訂閱模式自己實現了一套對象的事件系統Events,簡單來說Events可以讓對象擁有事件能力,其定義了比較豐富的API,并且如果你引入了backbone,這套事件系統還可以集成到自己的對象上,這是一個非常好的設計。
另外,源代碼中所有的以_開頭的方法,可以認為是私有方法,是沒有必要直接使用的,也不建議用戶覆蓋。
backbone模塊化處理、防止沖突和underscore混入代碼首先進行了區分使用環境(self或者是global,前者代表瀏覽器環境(self和window等價),后者代表node環境)和模塊化處理操作,之后處理了在AMD和CommonJS加載規范下的引入方式,并且明確聲明了對jQuery(或者Zepto)和underscore的依賴。
很遺憾的是,雖然backbone這樣做了,但是backbone并不適合在node端直接使用,也不適合服務端渲染,另外還和ES6相處的不是很融洽,這個我們后面還會陸續提到原因。
backbone noConflictbackbone也向jQuery致敬,學習了它的處理沖突的方式:
var previousBackbone = root.Backbone; //... Backbone.noConflict = function() { root.Backbone = previousBackbone; return this; };
這段代碼的邏輯非常簡單,我們可以通過以下方式使用:
var localBackbone = Backbone.noConflict(); var model = localBackbone.Model.extend(...);混入underscore的方法
backbone通過addUnderscoreMethods將一些underscore的實用方法混入到自己定義的幾個類中(注:確切地說是可供構造調用的函數,我們下文也會用類這個簡單明了的說法代替)。
這里面值得一提的是關于underscore的方法(underscore的源碼解讀請移步這里,fork from韓子遲),underscore的所有方法的參數序列都是固定的,也就是說第一個參數代表什么第二個參數代表什么,所有函數都是一致的,第一個參數一定代表目標對象,第二個參數一定代表作用函數(有的函數可能只有一個參數),在有三個參數的情況下,第三個參數代表上下文this,另外如果有第四個參數,第三個參數代表初始值或者默認值,第四個參數代表上下文。所以addMethod就是根據以上規定來使用的。
另外關于javascript中的this,我曾經寫過博客在這里,有興趣的可以看
混入方法的實現邏輯:
var addMethod = function(length, method, attribute) { //... }; var addUnderscoreMethods = function(Class, methods, attribute) { _.each(methods, function(length, method) { if (_[method]) Class.prototype[method] = addMethod(length, method, attribute); }); }; //之后使用: var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0, omit: 0, chain: 1, isEmpty: 1}; //混入一些underscore中常用的方法 addUnderscoreMethods(Model, modelMethods, "attributes");backbone Events
backbone的Events是一個對象,其中的方法(onlistenTooffstopListeningoncelistenToOncetrigger)都是對象方法。
總體上,backbone的Events實現了監聽/觸發/解除對自己對象本身的事件,也可以讓一個對象監聽/解除監聽另外一個對象的事件。
綁定對象自身的監聽事件on關于對象自身事件的綁定,這個比較簡單,除了最基本的綁定之外(一個事件一個回調),backbone還支持以下兩種方式的綁定:
//傳統方式 model.on("change", common_callback); //傳入一個名稱,回調函數的對象 model.on({ "change": on_change_callback, "remove": on_remove_callback }); //使用空格分割的多個事件名稱綁定到同一個回調函數上 model.on("change remove", common_callback);
這用到了它定義的一個中間函數eventsApi,這個函數比較實用,可以根據判斷使用的是哪種方式(實際上這個判斷也比較簡單,根據傳入的是對象判斷屬于上述第二種方式,根據正則表達式判斷是上述的第三種方式,否則就是傳統的方式)。然后再進行遞歸或者循環或者直接處理。
在對象中存儲事件實際上大概是下述形式:
events:{ change:[事件一,事件二] move:[事件一,事件二,事件三] }
而其中的事件實際上是一個整理好的對象,是如下形式:
{callback: callback, context: context, ctx: context || ctx, listening: listening}
這樣在觸發的時候,一個個調用就是了。
監聽其他對象的事件listenTobackbone還支持監聽其他對象的事件,比如,B對象上面發生b事件的時候,通知A調用回調函數A.listenTo(B, “b”, callback);,而這也是backbone處理非常巧妙的地方,我們來看看它是怎么做的。
實際上,這和B監聽自己的事件,并且在回調函數的時候把上下文變成A,是差不多的:B.on(“b”, callback, A);(on的第三個參數代表上下文)。
但是backbone還做了另外的事情,這里我們假設是A監聽B的一個事件(比如change事件好了)。
首先A有一個A._listeningTo屬性,這個屬性是一個對象,存放著它監聽的別的對象的信息A._listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0},這個id并不是數字,是每一個對象都有的唯一字符串,是通過_.uniqueId這個underscore方法生成的,這里的obj是B,objId是B的_listenId,id是A的_listenId,count是一個計數功能,而這個A._listeningTo[id]會被直接引用賦值到上面事件對象的listening屬性中。
為什么要多listenTo?Inversion of Control通過以上我們似乎有一個疑問,好像on就能把listenTo的功能搞定了,用一個listenTo純屬多余,并且許多其他的類庫也是只有一個on方法。
首先,這里會引入一個概念:控制反轉,所謂控制反轉,就是原來這個是B對象來控制的事件我們現在交由A對象來控制,那現在假設A分別listenTo B、C、D三個對象,那么這個時候假設A不監聽了,那么我們直接對A調用一個stopListening方法,則可以同時解除對B、C、D的監聽(這里我講的可能不是十分正確,這里另外推薦一個文章)。
另外,我們需要從backbone的設計初衷來看,backbone的重點是View、Model和Collection,實際上,backbone的View可以對應一個或者多個Collection,當然我們也可以讓View直接對應Model,但問題是View也并不一定對應一個Model,可能對應多個Model,那么這個時候我們通過listenTo和stopListening可以非常方便的添加、解除監聽。
//on的方式綁定 var view = { DoSomething :function(some){ //... } } model.on("change:some",view.DoSomething,view); model2.on("change:some",view.DoSomething,view); //解綁,這個時候要做的事情比較多且亂 model.off("change:some",view.DoSomething,view); model2.off("change:some",view.DoSomething,view); //listenTo的方式綁定 view.listenTo(model,"change:some",view.DoSomething); view.listenTo(model2,"change:some",view.DoSomething); //解綁 view.stopListening();
另外,在實際使用中,listengTo的寫法也的確更加符合用戶的習慣.
以下是摘自backbone官方文檔的一些解釋,僅供參考:
解除綁定事件off、stopListeningThe advantage of using this form, instead of other.on(event, callback, object), is that listenTo allows the object to keep track of the events, and they can be removed all at once later on. The callback will always be called with object as context.
與on不同,off的三個參數都是可選的
如果沒有任何參數,off相當于把對應的_events對象整體清空
如果有name參數但是沒有具體指定哪個callback的時候,則把這個name(事件)對應的回調隊列全部清空
如果還有進一步詳細的callback和context,那么這個時候移除回調函數非常嚴格,必須要求上下文和原來函數完全一致
off的最終實現函數是offApi,這個函數算上注釋有大概50行。
var offApi = function(events, name, callback, options) { //... }
這里面需要多帶帶提一下,前面有這樣的幾行:
if (!name && !callback && !context) { var ids = _.keys(listeners);//所有監聽它的對應的屬性 for (; i < ids.length; i++) { listening = listeners[ids[i]]; delete listeners[listening.id]; delete listening.listeningTo[listening.objId]; } return; }
這幾行是做了一件什么事呢?
刪除了所有的多對象監聽事件記錄,之后刪除自身的監聽事件。我們假設A監聽了B的一個事件,這個時候A._listenTo中就會多一個條目,存儲這個監聽事件的信息,而這個時候B的B._listeners也會多一個條目,存儲監聽事件的信息,注意這兩個條目都是按照id為鍵的鍵值對來存儲,但是這個鍵是不一樣的,值都指向同一個對象,這里刪除對這個對象的引用,之后就可以被垃圾回收機制回收了。如果這個時候調用B.off(),那么這個時候,以上的兩個條目都被刪除了。另外,注意最后的return,以及Events.off中的:
this._events = eventsApi(offApi, this._events, name, callback, { context: context, listeners: this._listeners });
所以如果B.off()這樣調用然后直接把 B._events 在之后也清空了,太巧妙了。
之后有一個對names(事件名)的循環(如果沒有指定,那么默認就是所有names),這個循環內容理解起來比較簡單,里面也順便照顧了_listeners_listenTo這些變量。這里不過多解釋了。
另外,stopListening實際上也是調用offApi,先處理了一下交給off函數,這也是設計模式運用典范(適配器模式)。
once和listenToOnce這兩個函數顧名思義,和on以及listenTo的區別不大,唯一的區別就是回調函數只供調用一次,多觸發調用也沒有用(實際上不會被觸發了)。
兩者都用到了onceMap這個函數,我們分析一下這個函數:
var onceMap = function(map, name, callback, offer) { if (callback) { //_.once:創建一個只能調用一次的函數。重復調用改進的方法也沒有效果,只會返回第一次執行時的結果。 作為初始化函數使用時非常有用, 不用再設一個boolean值來檢查是否已經初始化完成. var once = map[name] = _.once(function() { offer(name, once); callback.apply(this, arguments); }); //這個在解綁的時候有一個分辨效果 once._callback = callback; } return map; };
backbone的設計思路是這樣的:用_.once()創建一個只能被調用一次的函數,這個函數在第一次被觸發調用的時候,進行解除綁定(offer實際上是一個已經綁定好this的解除綁定函數,這個可以參見once和listenToOnce的源代碼),然后再調用callback,這樣既實現了調用一次的目的,也方便了垃圾回收。
其他和on以及listenTo的時候一樣,這里就不過多介紹了。
triggertrigger函數是用于觸發事件,支持多個參數,除了第一個參數以外,其他的參數會依次放入觸發事件的回調函數的參數中(backbone默認對3個參數及以下的情況下進行call調用,這種處理方式原因之一是call調用比apply調用的效率更高從而優先使用(關于call和apply的性能對比:https://jsperf.com/call-apply...),另外一方面源碼中并沒有超過三個參數的情況,所以用call支持到了三個參數,其余情況采用性能較差但是寫起來方便的apply)。
另外值得一提的是,Events支持all事件,即如果你監聽了all事件,那么任何事件的觸發都會調用all事件的回調函數列。
關于trigger部分的源代碼比較簡單,并且我也增加了一些評注,這里就不貼代碼了。
context 和 ctx有心的朋友也許注意到,backbone在事件中用到了context和ctx這兩個"貌似"表示當前上下文的對象,并且在如果有context的情況下,這兩個幾乎一樣:
handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening});
這里我根據自己的理解,盡量解釋一下。
我們可以主要看off方法及trigger方法,我們發現上面兩屬性在這兩個方法中分別被使用了。
在off里需要對context進行比較決定是否要刪除對應的事件,所以model._events中保存下來的context,必須是未做修改的。
而trigger里在執行回調函數時,需要指定其作用域,當綁定事件時沒有給定作用域,則會使用被監聽的對象當回調函數的作用域。
實際上,我覺得這個ctx有點多余,我們完全可以在trigger中這樣寫:
(ev = events[i]).callback.call(ev.context || ev.obj)backbone Model
backbone的Model實際上是一個可供構造調用的函數,backbone采用污染原型的方式把定義好的屬性都定義在了prototype上,這可能并不是一個非常妥當的做法,但是在backbone中這樣做卻是沒有什么不可以的,這個我們在之后講extend方法的時候會進行補充。
我們先看看這個函數在實例化的時候會做點什么:
var Model = Backbone.Model = function(attributes, options) { var attrs = attributes || {}; options || (options = {}); //這個preinitialize函數實際上是為空的,可以給有興趣的開發者重寫這個函數,在初始化Model之前調用 this.preinitialize.apply(this, arguments); //Model的唯一的id this.cid = _.uniqueId(this.cidPrefix); this.attributes = {}; if (options.collection) this.collection = options.collection; //如果之后new的時候傳入的是JSON,我們必須在options選項中聲明parse為true if (options.parse) attrs = this.parse(attrs, options) || {}; //_.result:如果指定的property的值是一個函數,那么將在object上下文內調用它;否則,返回它。如果提供默認值,并且屬性不存在,那么默認值將被返回。如果設置defaultValue是一個函數,它的結果將被返回。 //這里調用_.result相當于給出了余地,自己寫defaults的時候可以直接寫一個對象,也可以寫一個函數,通過return一個對象的方式把屬性包含進去 var defaults = _.result(this, "defaults"); //defaults應該是在Backbone.Model.extends的時候由用戶添加的,用defaults對象填充object 中的undefined屬性。 并且返回這個object。一旦這個屬性被填充,再使用defaults方法將不會有任何效果。 attrs = _.defaults(_.extend({}, defaults, attrs), defaults); this.set(attrs, options); //存儲歷史變化記錄 this.changed = {}; //這個initialize也是空的,給初始化之后調用 this.initialize.apply(this, arguments); };
我們可以看出,this.attributes是存儲實際內容的。
另外,preinitialize和initialize不僅在Model中有,在之后的Collection、View和Router中也都出現了,一個是在初始化前調用,另外一個是在初始化之后調用。
關于preinitialize的問題,我們后文還要繼續討論,它的出現和ES6有關。
Model setModel的set方法是一個重點的方法,這個方法的功能比較多,本身甚至還可以刪除屬性,因為unset內部和clear的內部等也調用了set方法。在用戶手動賦值的時候,支持下面兩種賦值方式:"key", value 和{key: value}兩種賦值方式。
我們分析這個函數總共做了哪些事情:
對兩種賦值方式的支持"key", value和{key: value}的預處理。
如果你寫了validate驗證函數沒有通過驗證,那么就不繼續做了(需要顯式聲明使用validate)。
進行變量的更改或者刪除,順便把歷史版本的問題解決掉。
如果不是靜默set的,那么這個時候開始進行change事件的觸發。
具體這一塊注釋筆者寫的非常詳細,所以在這里也不再贅述。
fetch、save、destroy這幾個功能是需要跟服務端交互的,所以我們放在一起來分析一下。
backbone通過封裝好模型和服務器交互的函數,大大方便了開發者和服務端數據同步的工作,當然,這需要一個對應的后端,不僅需要支持POST、PUT、PATCH、DELETE、GET多種請求,甚至連url的格式都給定義好了,url的格式為:yourUrl/id,這個id肯定是需要我們傳入的,并且要求跟服務器上的id對應(畢竟服務器要識別處理)
注意:url并不一定非要按照backbone的來,我們完全可以調用這幾個方法的時候再指定一個url{url:myurl,success:successFunction},這個部分backbone 在sync函數中進行了一個判斷處理,優先選擇后指定的url,不過這樣對我們來說是比較麻煩的,也并不符合backbone的設計初衷
這三個函數最后都用到了sync函數,所以我們要先分析sync函數:
Backbone.sync = function(method, model, options) { //... }; Backbone.ajax = function() { return Backbone.$.ajax.apply(Backbone.$, arguments); };
sync函數在其中調用了ajax函數,而ajax函數就是jQuery的ajax,這個我們非常熟悉,它可以插入非常多的參數,我們可以這里查看文檔。
另外,這個sync支持兩個特殊情況:
emulateHTTP:如果你想在不支持Backbone的默認REST/ HTTP方式的Web服務器上工作, 您可以選擇開啟Backbone.emulateHTTP。 設置該選項將通過 POST 方法偽造 PUT,PATCH 和 DELETE 請求 用真實的方法設定X-HTTP-Method-Override頭信息。 如果支持emulateJSON,此時該請求會向服務器傳入名為 _method 的參數。
emulateJSON:如果你想在不支持發送 application/json 編碼請求的Web服務器上工作,設置Backbone.emulateJSON = true;將導致JSON根據模型參數進行序列化, 并通過application/x-www-form-urlencoded MIME類型來發送一個偽造HTML表單請求
具體的這個sync方法,就是構造ajax參數的過程。
fetch可以傳入一個回調函數,這個回調函數會在ajax的回調函數中被調用,另外ajax的回調函數是在fetch中定義的,這個回調函數做了這樣幾件事情:
options.success = function(resp) { //處理返回數據 var serverAttrs = options.parse ? model.parse(resp, options) : resp; //根據服務器返回數據設置模型屬性 if (!model.set(serverAttrs, options)) return false; //觸發自定義回調函數 if (success) success.call(options.context, model, resp, options); //觸發事件 model.trigger("sync", model, resp, options); };
save方法為向服務器提交保存數據的請求,如果是第一次保存,那么就是POST請求,如果不是第一次保存數據,那么就是PUT請求。
其中,傳遞的options中可以使用的字段以及意義為:
wait: 可以指定是否等待服務端的返回結果再更新model。默認情況下不等待
url: 可以覆蓋掉backbone默認使用的url格式
attrs: 可以指定保存到服務端的字段有哪些,配合options.patch可以產生PATCH對模型進行部分更新
patch:boolean 指定使用部分更新的REST接口
success: 自己定義一個回調函數
data: 會被直接傳遞給jquery的ajax中的data,能夠覆蓋backbone所有的對上傳的數據控制的行為
其他: options中的任何參數都將直接傳遞給jquery的ajax,作為其options
關于save函數具體的處理邏輯,我在源代碼中添加了非常詳細的注釋,這里就不展開了。
銷毀這個模型,我們可以分析,銷毀模型要做以下幾件事情:
停止對該對象所有的事件監聽,本身都沒有了,還監聽什么事件
告知服務器自己要被銷毀了(如果isNew()返回true,那么其實不用向服務器發送請求)
如果它屬于某一個collection,那么要告知這個collection要把這個模型移除
其中,傳遞的options中可以使用的字段以及意義為:
wait: 可以指定是否等待服務端的返回結果再銷毀。默認情況下不等待
success: 自己定義一個回調函數
Model的其他內容另外值得一提的是,Model是要求傳入的id唯一的,但是對這個id如果重復的情況下的錯誤處理做的不是很到位,所以有的時候你看控制臺報錯并不能及時發現問題。
backbone CollectionCollection也是一個可供構造調用的函數,我們還是先看看這個Collection做了些什么:
var Collection = Backbone.Collection = function(models, options) { options || (options = {}); this.preinitialize.apply(this, arguments); //實際上我們在創建集合類的時候大多數都會定義一個model, 而不是在初始化的時候從options中指定model if (options.model) this.model = options.model; //我們可以在options中指定一個comparator作為排序器 if (options.comparator !== void 0) this.comparator = options.comparator; //_reset用于初始化 this._reset(); this.initialize.apply(this, arguments); //如果我們在new構造調用的時候聲明了models,這個時候需要調用reset函數 if (models) this.reset(models, _.extend({silent: true}, options)); };
實際上,我覺得backbone的Model、View、Collection里的邏輯還是比較清楚的,可讀性也比較強,所以主要就是把注釋寫在代碼里面。
Collection setcollection的一個核心方法,內容很長,我們可以把它理解為重置:給定一組新的模型,增加新的,去除不在這里面的(在添加模式下不去除),混合已經存在的。但是這個方法同時也很靈活,可以通過參數的設定來改變模式
set可能有如下幾個調用場景:
重置模式,這個時候不在models里的model都會被清除掉。對應上文的:var setOptions = {add: true, remove: true, merge: true};
添加模式,這個時候models里的內容會做添加用,如果有重復的(cid來判斷),會覆蓋。對應上文的:var addOptions = {add: true, remove: false};
我們還是理一理里面做了哪些事情:
先規范化models和options兩個參數
遍歷models:
如果是重置模式,那么遇到重復的就直接覆蓋掉,并且也添加到set隊列,遇到新的就先添加到set隊列。之后還要刪除掉models里沒有而原來collection里面有的
如果是添加模式,那么遇到重復的,就先添加到set隊列,遇到新的也是添加到set隊列
之后進行整理,整合到collection中(可能會觸發排序操作)
如果不是靜默處理,這個時候會觸發各類事件
當然,我們在進行調用的時候,是不需要考慮這么復雜的,這個函數之所以做的這么復雜,是因為它也供許多內置的其他函數調用了,這樣可以減少重復代碼的冗余,符合函數式編程的思想。另外set函數雖然繁雜卻不贅余,里面定義的函數內變量邏輯都有自己的作用。
sort上文中提到了sort函數,sort所依據的是用戶傳入的comparator參數,這個參數可以是一個字符串表示的單個屬性也可以是一個函數,另外也可以是一個多個屬性組成的數組,如果是單個屬性或者函數,就調用underscore的排序方法,如果是一個多個屬性組成的數組,就調用原生的數組排序方法(原生方法支持按照多個屬性分優先級進行排序)
fetch、create這是Collection中涉及到和服務端交互的方法,這兩個方法非常有區別。
fetch是直接從服務器拉取數據,并沒有調用model的fetch方法,返回的數據格式應當是直接可以調用上文的set函數的數據格式,另外值得注意的是,想要調用這個方法,一定要先指定url
create是指將特定的model上傳到服務器上去,并沒有調用自己的方法而是最后調用了model自身的方法model.save(null, options),這里第一個參數被賦值成null還是有意義的,我們通過分析save函數前幾行代碼就可以很明顯地分析出原因。
CollectionIterator這是一個基于ES6的新的內容,目的是創建一個遍歷器,之后,我們可以在collection的一些方法中運用這個可遍歷對象。
這個方面的知識可以看這里補充,三言兩語也無法說清,簡單地講,就是如果正確地定義了一個next屬性方法,這個對象就可以按照自己定義的方式來遍歷了。
而backbone這里定義的這個遍歷器更加強大,可以分別按照key、value、key和value三種方式遍歷
我這里給出一個使用方式:
window.Test = Backbone.Model.extend({ defaults: {content: "" } }); // 創建集合模型類 window.TestList = Backbone.Collection.extend({ model: Test }); // 向模型添加數據 var data = new TestList( [ { id:100, content: "hello,backbone!" }, { id:101, content: "hello,Xiaotao!" } ] ); for(var ii of data.keys()){ console.log(ii); } for( ii of data.values()){ console.log(ii); } for( ii of data.entries()){ console.log(ii); }
具體這里是如何實現的,我相信大家看了上文鏈接給出的擴展知識之后,然后再結合我寫了注釋的源代碼,應該都能看懂了。
Collection其他內容另外,Collection還實現了非常多的小方法,也混入了很多underscore的方法,但核心都是操作this.models,this.models是一個正常的數組(所以,在js中本身實現了的方法也是可以在這里使用的),可以直接訪問。
另外值得一提的是,Collection中有一個_byId變量,這個變量通過cid和id來存取,起到一個方便直接存取的作用,在某些時候非常方便。
_addReference: function(model, options) { this._byId[model.cid] = model; var id = this.modelId(model.attributes); if (id != null) this._byId[id] = model; model.on("all", this._onModelEvent, this); },
另外實際上,model除了作為Collection里面的元素,并且通過一個collection屬性指向對應的Collection,實際上聯系也并不是非常多,這也比較符合低耦合高內聚的策略。
backbone View接下來我們進入backbone的View部分,也就是和用戶打交道的部分,我一開始用backbone的時候就是被View層可以通過定義events對象數組來方便地進行事件管理所吸引(雖然現在看來還有更方便的方案)
我們先來看一下View函數在用戶新建View的時候做了些什么:
var View = Backbone.View = function(options) { this.cid = _.uniqueId("view"); this.preinitialize.apply(this, arguments); //_.pick(object, *keys):返回一個object副本,只過濾出keys(有效的鍵組成的數組)參數指定的屬性值。或者接受一個判斷函數,指定挑選哪個key。 _.extend(this, _.pick(options, viewOptions)); //初始化dom元素和jQuery元素工作 this._ensureElement(); //自定義初始化函數 this.initialize.apply(this, arguments); };
這里面值得一提的是this._ensureElement()這個函數,這個函數內部調用了很多函數,做了很多工作,我們首先看這個函數:
_ensureElement: function() { if (!this.el) { var attrs = _.extend({}, _.result(this, "attributes")); if (this.id) attrs.id = _.result(this, "id"); if (this.className) attrs["class"] = _.result(this, "className"); this.setElement(this._createElement(_.result(this, "tagName"))); this._setAttributes(attrs); } else { this.setElement(_.result(this, "el")); } },
根據你是否傳入一個dom元素(這個dom元素用來和View對應,也可以是jQuery元素)分成了兩種情況執行,我們先看不傳入的情況:
這個時候我們可以定義一些屬性,這些屬性都在接下來賦值到生成的dom對象上:
_setAttributes: function(attributes) { this.$el.attr(attributes); }
接下來看假設傳入了了的情況:
setElement: function(element) { this.undelegateEvents(); this._setElement(element); this.delegateEvents(); return this; },
這里面又調用了三個函數,我們看一下這三個函數:
undelegateEvents: function() { if (this.$el) this.$el.off(".delegateEvents" + this.cid); return this; }, _setElement: function(el) { this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); this.el = this.$el[0]; }, delegateEvents: function(events) { events || (events = _.result(this, "events")); if (!events) return this; this.undelegateEvents(); for (var key in events) { var method = events[key]; if (!_.isFunction(method)) method = this[method]; if (!method) continue; var match = key.match(delegateEventSplitter); this.delegate(match[1], match[2], _.bind(method, this)); } return this; }, delegate: function(eventName, selector, listener) { this.$el.on(eventName + ".delegateEvents" + this.cid, selector, listener); return this; },
上面第四個函數為第三個函數所調用的,因此我們放在了一起。
第一個函數是解綁backbone所用的jQuery事件命名空間下的事件(.delegateEvents),這個是方式這個事件被之前的其他View使用過,從而造成污染(實際上,這個一般情況下用的是不多的)。
第二個函數是初始化dom對象和jQuery對象,$el代表jQuery對象,el代表dom對象。
第三個函數是把我們寫的監聽事件進行重新綁定,我們寫的事件滿足下面的格式:
//舉個例子: { "mousedown .title": "edit", "click .button": "save", "click .open": function(e) { ... } }
上面第三個函數就是一個解析函數,解析好后直接調用delegate函數進行事件的綁定,這里要注意你定義的事件的元素必須在提供的el內的,否則無法訪問到。
render另外,backbone中有一個render函數:
render: function() { return this; },
這個render函數實際上有比較深遠的意義,render函數默認是沒有操作的,我們可以自己定義操作,然后可以在事件中"change" "render"這樣對應,這樣每次變化就會重新調用render重繪,我們也可以自定義好render函數并且在初始化函數initialize中調用。另外,render函數默認的return this;隱含了backbone的一種期望:返回this從而支持鏈式調用。
render可以使用underscore的模版,并且這也是推薦做法,以下是一個非常簡單的demo:
var Bookmark = Backbone.View.extend({ template: _.template(...), render: function() { this.$el.html(this.template(this.model.attributes)); return this; } });backbone router、history router
backbone相比于一些流行框架的好處就是自己實現了router部分,不用再引入其他插件,這點十分方便。
我們在使用router的時候,通常會采用如下寫法:
var Workspace = Backbone.Router.extend({ routes: { "help": "help", // #help "search/:query": "search", // #search/kiwis "search/:query/p:page": "search" // #search/kiwis/p7 }, help: function() { ... }, search: function(query, page) { ... } });
router的供構造調用的函數的主體部分也相當簡單,沒有做多余的事情:
var Router = Backbone.Router = function(options) { options || (options = {}); this.preinitialize.apply(this, arguments); //注意這個地方,options的routes會直接this的routes,所以如果在建立類的時候指定routes,實例化的時候又擴展了routes,是會被覆蓋的 if (options.routes) this.routes = options.routes; //對自己定義的路由進行處理 this._bindRoutes(); //調用自定義初始化函數 this.initialize.apply(this, arguments); };
這里我們展開_bindRoutes:
_bindRoutes: function() { if (!this.routes) return; this.routes = _.result(this, "routes"); var route, routes = _.keys(this.routes); while ((route = routes.pop()) != null) { this.route(route, this.routes[route]); } },
route函數是把路由處理成正則表達式形式,然后調用history.route函數進行綁定,history.route函數在網址每次變化的時候都會檢查匹配,如果有匹配就執行回調函數,也就是下文Backbone.history.route傳入的第二個參數,這樣路由部分和history部分就聯系在一起了。
route: function(route, name, callback) { //如果不是正則表達式,轉換之 if (!_.isRegExp(route)) route = this._routeToRegExp(route); if (_.isFunction(name)) { callback = name; name = ""; } if (!callback) callback = this[name]; var router = this; Backbone.history.route(route, function(fragment) { var args = router._extractParameters(route, fragment); if (router.execute(callback, args, name) !== false) { router.trigger.apply(router, ["route:" + name].concat(args)); router.trigger("route", name, args); Backbone.history.trigger("route", router, name, args); } }); return this; },
上面的這段代碼首先可能會調用_routeToRegExp這個函數進行正則處理,這個函數可能是backbone中最難懂的函數,不過不懂也并不影響我們繼續分析(實際上,筆者也并沒有完全懂這個函數,所以希望經驗人士可以在這里給予幫助)。
_routeToRegExp: function(route) { route = route.replace(escapeRegExp, "$&")//這個匹配的目的是將正則表達式字符進行轉義 .replace(optionalParam, "(?:$1)?") .replace(namedParam, function(match, optional) { return optional ? match : "([^/?]+)"; }) .replace(splatParam, "([^?]*?)"); return new RegExp("^" + route + "(?:?([sS]*))?$"); },
另外調用了_extractParameters這個函數和router.execute這個函數,前者的作用就是將匹配成功的URL中蘊含的參數轉化成一個數組返回,后者接受三個參數,分別是回調函數,參數列表和函數名(這里之前只有兩個函數,后來backbone增加了第三個參數)。
_extractParameters: function(route, fragment) { var params = route.exec(fragment).slice(1); return _.map(params, function(param, i) { // Don"t decode the search params. if (i === params.length - 1) return param || null; return param ? decodeURIComponent(param) : null; }); } execute: function(callback, args, name) { if (callback) callback.apply(this, args); },
router的內容也就這些了,實現的比較簡單清爽,代碼也不多,關于處理歷史記錄瀏覽器兼容性的問題都放在了history部分,所以接下來我們來分析難啃的history部分。
history這一塊的內容比較重要,并且相比于之前的內容有些復雜,我盡量把自己的理解全都講解出來。
我們先說明一下這個歷史記錄的作用:
當你在瀏覽器訪問的時候,可以通過左上角的前進后退進行切換,這就是因為產生了歷史記錄。
那么什么方式可以產生歷史記錄呢?
頁面跳轉(肯定的,但是并不適用于SPA)
hash變化:形如這種點擊后會觸發歷史記錄),但是不幸的是在IE7下并不能被寫入歷史記錄(雖然筆者是對IE9以下堅決說不的)
pushState,這種比較牛逼,可以默默的改變路由,比如把article.html#article/54改成article.html#article/53但是不觸發頁面的刷新,因為一般情況下這算是兩個頁面的,另外,這種情況需要服務端的支持,因此我在用backbone的時候較少采用這種做法(現在有一個概念叫做pjax,就是ajax+pushState,具體可以Google之)
iframe內url變化,變化iframe內的url也會觸發歷史記錄,但是這個比較麻煩,另外,在IE中,無論iframe是一開始靜態寫在html中的還是后來用js動態創建的,都可以被寫入瀏覽器的歷史記錄,其他瀏覽器一般只支持靜態寫在html中。所以,我們一般在2&3都不可用的情況下,才選用這種情況(IE7以下)
以上講的基本就是backbone使用的方式,接下來我們再按照backbone使用邏輯和優先級進行一些講解:
backbone默認是使用hash的,在不支持hash的瀏覽器中使用iframe,如果想要使用pushState,需要顯式聲明并且瀏覽器本身要支持(如果使用了pushState的話hash就不用了)。
所以backbone的history有一個非常大的start函數,這個函數從頭到尾做了如下幾件事情:
將頁面的根部分保存在root中,默認是/
判斷是否想用hashChange(默認為true)以及支持與否,判斷是否想用pushState以及支持與否。
判斷一下到底是用hash還是用push,并且做一些url處理
如果需要用到iframe,這個時候初始化一下iframe
初始化監聽事件:用hash的話可以監聽hashchange事件,用pushState的話可以監聽popState事件,如果用了iframe,沒辦法,只能輪詢了,這個主要是用來用戶的前進后退。
最后最重要的:先處理以下當前頁面的路由,也就是說,假設用戶直接訪問的并不是根頁面,不能什么也不做呀,要調用相關路由對應的函數,所以這里要調用loadUrl
和start對應的stop函數,主要做了一些清理工作,如果能讀懂start,那么stop函數應該是不難讀懂的。
另外還有一個比較長的函數是navigate,這個函數的作用主要是存儲/更新歷史記錄,主要和瀏覽器打交道,如果用hash的話,backbone自身是不會調用這個函數的(因為用不到),但是可以供開發者調用:
開發者可以通過這個函數用js代碼自動管理路由:
openPage: function(pageNumber) { this.document.pages.at(pageNumber).open(); this.navigate("page/" + pageNumber); }
另外,backbone在這一部分定義了一系列工具函數,用于處理url。
backbone的history這一部分寫的非常的優秀,兼容性也非常的高,并且充分滿足了高聚合低耦合的特點,如果自己也要實現history管理這一部分,那么backbone的這個history絕對是一個優秀的范例。
extend最后,backbone還定義了一個extend函數,這個函數我們再熟悉不過了,不過它的寫法并沒有我們想象的那么簡單,
這個函數并沒有直接將屬性assign到parent上面(this),是因為這樣會產生一個顯著的問題:污染原型
所以實際上backbone的做法是新建了一個子類,這個子對象承擔著所有內容.
另外,這個extend函數也借鑒了ES6的一些寫法,內容不多,理解起來也是簡單的。
ES6&backbonebackbone支持ES6的寫法,關于這個寫法問題,曾經GitHub上面有過激烈的爭論,這里我稍作總結,先給出一個目前可行的寫法:
class DocumentRow extends Backbone.View { preinitialize() { _.extend(this, { tagName: "li", className: "document-row", events: { "click .icon": "open", "click .button.edit": "openEditDialog", "click .button.delete": "destroy" } }); } initialize() { this.listenTo(this.model, "change", this.render); } render() { //... } }
實際上,這個問題出現之前backbone的源代碼中是沒有preinitialize函數的,關于為什么最終是這樣,我總結以下幾點:
ES6的class不能直接寫屬性(直接報錯),都要寫成函數,因為如果有屬性的話會出現共享屬性的問題。
ES6的class寫法和ES5的不一樣,也和backbone自己定義的extend是不一樣的。是先要調用父類的構造方法,然后再有子類的this,在調用constructor之前是無法使用this的。所以下面這種寫法就不行了:
class DocumentRow extends Backbone.View { constructor() { this.tagName = "li"; this.className = "document-row"; this.events = { "click .icon": "open", "click .button.edit": "openEditDialog", "click .button.delete": "destroy" }; super(); } initialize() { this.listenTo(this.model, "change", this.render); } render() { //... } }
但是如果把super提前,那么這個時候tagName什么的還沒有賦值呢,element就已經建立好了。
另外,把屬性強制寫成函數的做法是被backbone支持的,但是我相信沒有多少人愿意這樣做吧:
class DocumentRow extends Backbone.View { tagName() { return "li"; } className() { return "document-row";} events() { return { "click .icon": "open", "click .button.edit": "openEditDialog", "click .button.delete": "destroy" }; } initialize() { this.listenTo(this.model, "change", this.render); } render() { //... } }
所以我們需要:及早把一些屬性賦給父類覆蓋掉父類默認屬性,然后調用父類構造函數,然后再調用子類構造函數。所以加入一個preinitialize方法是一個比較好的選擇。
如果還沒有理解,不妨看看下面這個本質等價的小例子:
class A{ constructor(){ this.s=1; this.preinit(); this.dosomething(); this.init(); } preinit(){} init(){} dosomething(){console.log("dosomething:",this.s)}//dosomething 2 } class B extends A{ preinit(){this.s=2;} init(){} } var b1 = new B(); console.log(b1.s);//2總結
經過以上漫長的對backbone源代碼分析的過程,我們了解了一個優秀的框架的源代碼,我總結了backbone源碼的幾個特點如下:
充分發揮函數式編程的精神,符合函數式編程,之前有位前輩說對js的運用程度就取決于對js的函數式編程的認識程度,也是不無道理的。
高內聚低耦合可擴展,這一方面方便了我們使用backbone的一部分內容(比如只使用Events或者router),另外一方面也方便了插件開發,以及能和其他的庫比較好的兼容,我認為,這并不是一個強主張的庫,你可以小規模地按照自己的方式使用,也可以大規模的完全按照backbone的期望使用。
在使用和兼容ES6的新特性上做了不少努力,在源代碼中好幾處都體現了ES6的內容,這讓backbone作為一個老牌框架,在如今大規模使用做網頁應用,依然十分可行。
缺點:
backbone嚴重依賴jQuery和underscore,這對backbone起到了牽制作用,假設jQuery或者underscore改變了一個方法或者一個接口,那么backbone也要跟著改,另外backbone依賴的jQuery和underscore也有一些限制,直接隨便引入這三個文件很可能就會報錯(一般情況下都引入最新的是沒有問題的),這是backbone比較不好的一個地方(要不然自身也不可能做到這么輕量級)
--
參考資料
backbone官方文檔:http://backbonejs.org/
backbone中文文檔:http://www.css88.com/doc/back...
Why Backbone.js and ES6 Classes Don"t Mix:http://benmccormick.org/2015/...
關于backbone&ES6的討論:
https://github.com/jashkenas/...
https://github.com/jashkenas/...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/50390.html
摘要:接受個參數,包括事件的名稱,回調函數和回調函數執行的上下文環境。保留回調函數在數組中取出對應的以及中的函數。當然,你同樣可以在綁定的回調函數執行前手動通過將其移除。 Backbone源碼解讀 Backbone在流行的前端框架中是最輕量級的一個,全部代碼實現一共只有1831行1。從前端的入門再到Titanium,我雖然幾次和Backbone打交道但是卻對它的結構知之甚少,也促成了我想讀...
摘要:事件關于路由觸發事件是通過兩個函數來完成的,它們分別是和前者會檢測路由是否發生了改變,如果改變了就會觸發函數并調用函數,而后者會通過路由片段來找到相關的事件函數來觸發。 注意:強烈建議一邊閱讀源碼一邊閱讀本文。 終于到了backbone源碼解讀的最后一篇,這一篇和前面幾篇時間上有一定的間隔(因為要回學校有一堆亂七八糟的事...)。在這一篇里面會講解Bakcbone的sync & rou...
1. 開場 1.1 MVC? MVC是一種GUI軟件的一種架構模式。它的目的是將軟件的數據層(Model)和視圖(view)分開。Model連接數據庫,實現數據的交互。用戶不能直接和數據打交道,而是需要通過操作視圖,然后通過controller對事件作出響應,最后才得以改變數據。最后數據改變,通過觀察者模式更新view。(所以在這里需要用到設計模式中的觀察者模式) 1.2 Smalltalk-80...
摘要:以為例構造函數的內容構造函數的內部一般會做以下幾個操作各種給內部對象設置屬性。為什么呢源碼做出了解釋。在里面會調用用戶傳入的回調函數并觸發事件表示已經同步了。整個的源碼事實上就是這兩組東西。 1. 開場 強烈建議一邊看著源碼一邊讀本文章,本文不貼大段代碼。源碼地址。在寫backbone應用的時候,說實話,大部分的時間都是在寫這三個模塊的內容。關于這三個模塊的分析網上隨隨便便就能找到一堆...
摘要:個人認為,讀懂老牌框架的源代碼比會用流行框架的要有用的多。另外,源代碼中所有的以開頭的方法,可以認為是私有方法,是沒有必要直接使用的,也不建議用戶覆蓋。 寫在前面 backbone是我兩年多前入門前端的時候接觸到的第一個框架,當初被backbone的強大功能所吸引(當然的確比裸寫js要好得多),雖然現在backbone并不算最主流的前端框架了,但是,它里面大量設計模式的靈活運用,以及令...
閱讀 1391·2023-04-26 03:04
閱讀 2326·2019-08-30 15:44
閱讀 3727·2019-08-30 14:15
閱讀 3507·2019-08-27 10:56
閱讀 2703·2019-08-26 13:53
閱讀 2616·2019-08-26 13:26
閱讀 3075·2019-08-26 12:11
閱讀 3609·2019-08-23 18:21