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