前段時間曾經在InfoQ中文站上發表文章,介紹了dojo類機制的基本用法。有些朋友在讀後希望能夠更深入瞭解這部分的內容,本文將會介紹dojo類機制幕後的知識,其中會涉及到dojo類機制的實現原理並對一些關鍵方法進行源碼分析,當然在此之前希望您能夠對JavaScript和dojo的使用有些基本的瞭解。
dojo的類機制支援類聲明、繼承、調用父類方法等功能。dojo在底層實現上是通過操作原型鏈來實現其類機制的,而在實現繼承時採用類式繼承的方式。值得一提的是,dojo的類機制允許進行多繼承(注意,只有父類列表中的第一個作為真正的父類,其它的都是將其屬性以mixin的方法加入到子類的原型鏈中),為解決多重繼承時類方法的順序問題,dojo用JavaScript實現了Python和其它多繼承語言所支援的C3父類線性化演算法,以實現線性繼承關係,想瞭解更多該演算法的知識,可參考這裡,我們在後面的分析中將會簡單講解dojo對此演算法的實現。
1. dojo類聲明概覽
dojo類聲明相關的代碼位於“/dojo/_base/declare.js”檔案中,定義類是通過dojo.declare方法來實現的。關於這個方法的基本用法,已經在dojo類機制簡介這篇文章中進行了闡述,現在我們看一下它的實現原理(在這部分的程式碼分析中,會在整體上介紹dojo如何聲明類,後文會對裡面的重要細節內容進行介紹):
//此即為dojo.declare方法的定義d.declare = function(className, superclass, props){ //前面有格式化參數相關的操作,一般情況下定義類會把三個參數全傳進來,分別為//類名、父類(可以為null、某個類或多個類組成的數組)和要聲明類的屬性及方法 //定義一系列的變數供後面使用 var proto, i, t, ctor, name, bases, chains, mixins = 1, parents = superclass; // 處理要聲明類的父類 if(opts.call(superclass) == "[object Array]"){ //如果父類參數傳過來的是數組,那麼這裡就是多繼承,要用C3演算法處理父類的關係 //得到的bases為數組,第一個元素能標識真正父類(即superclass參數中的第一個)//在數組中的索引,其餘的數組元素是按順序排好的繼承鏈,後面還會介紹到C3演算法 bases = c3mro(superclass, className); t = bases[0]; mixins = bases.length - t; superclass = bases[mixins]; }else{ //此分支內是對沒有父類或單個父類情況的處理,不再詳述 } //以下為構建類的原型屬性和方法 if(superclass){ for(i = mixins - 1;; --i){ //此處遍曆所有需要mixin的類 //注意此處,為什麼說多個父類的情況下,只有第一個父類是真正的父類呢,因//為在第一次迴圈的執行個體化了該父類,並記在了原型鏈中,而其它需要mixin的//父類在後面處理時會把superclass設為一個空的構造方法,合并父類原型鏈//後進行執行個體化proto = forceNew(superclass); if(!i){ //此處在完成最後一個父類後跳出迴圈 break; } // mix in properties t = bases[i];//得到要mixin的一個父類 (t._meta ? mixOwn : mix)(proto, t.prototype);//合并原型鏈 // chain in new constructor ctor = new Function;//聲明一個新的Function ctor.superclass = superclass; ctor.prototype = proto;//設定原型鏈//此時將superclass指向了這個新的Function,再次進入這個迴圈的時候,執行個體//化的是ctor,而不是mixin的父類 superclass = proto.constructor = ctor; } }else{ proto = {}; } //此處將上面得到的方法(及屬性)與要聲明類本身所擁有的方法(及屬性)進行合并 safeMixin(proto, props); ………… //此處收集鏈式調用相關的資訊,後面會詳述 for(i = mixins - 1; i; --i){ // intentional assignment t = bases[i]._meta; if(t && t.chains){ chains = mix(chains || {}, t.chains); } } if(proto["-chains-"]){ chains = mix(chains || {}, proto["-chains-"]); } //此處根據上面收集的鏈式調用資訊和父類資訊構建最終的構造方法,後文詳述 t = !chains || !chains.hasOwnProperty(cname); bases[0] = ctor = (chains && chains.constructor === "manual") ? simpleConstructor(bases) : (bases.length == 1 ? singleConstructor(props.constructor, t) : chainedConstructor(bases, t)); //在這個構造方法中添加了許多的屬性,在進行鏈式調用以及調用父類方法等處會用到 ctor._meta = {bases: bases, hidden: props, chains: chains, parents: parents, ctor: props.constructor}; ctor.superclass = superclass && superclass.prototype; ctor.extend = extend; ctor.prototype = proto; proto.constructor = ctor; // 對於dojo.declare方法聲明類的執行個體均有以下的工具方法 proto.getInherited = getInherited; proto.inherited = inherited; proto.isInstanceOf = isInstanceOf; // 此處要進行全域註冊 if(className){ proto.declaredClass = className; d.setObject(className, ctor); } //對於鏈式調用父類的那些方法進行處理,實際上進行了重寫,後文詳述 if(chains){ for(name in chains){ if(proto[name] && typeof chains[name] == "string" && name != cname){ t = proto[name] = chain(name, bases, chains[name] === "after"); t.nom = name; } } } return ctor;//Function};
以上簡單介紹了dojo聲明類的整體流程,但是一些關鍵的細節如C3演算法、鏈式調用在後面會繼續進行介紹。
2. C3演算法的實現
通過以前的文章和上面的分析,我們知道dojo的類聲明支援多繼承。在處理多繼承時,不得不面對的就是繼承鏈如何構造,比較現實的問題是如果多個父類都擁有同名的方法,那麼在調用父類方法時,要按照什麼規則確定調用哪個父類的呢?在解決這個問題上dojo實現了C3父類線性化的方法,對多個父類進行合理的排序,從而完美解決了這個問題。
為了瞭解繼承鏈的相關知識,我們看一個簡單的例子:
dojo.declare("A",null);dojo.declare("B",null);dojo.declare("C",null);dojo.declare("D",[A, B]);dojo.declare("E",[B, C]); dojo.declare("F",[A, C]); dojo.declare("G",[D, E]);
以上的代碼中,聲明了幾個類,通過C3演算法得到G的繼承順序應該是這樣G->E->C->D->B->A的,只有按照這樣的順序才能保證類定義和依賴是正確的。那我們看一下這個C3演算法是如何?的呢:
function c3mro(bases, className){ //定義一系列的變數 var result = [], roots = [{cls: 0, refs: []}], nameMap = {}, clsCount = 1, l = bases.length, i = 0, j, lin, base, top, proto, rec, name, refs; //在這個迴圈中,構建出了父類各自的依賴關係(即父類可能會依賴其它的類) for(; i < l; ++i){ base = bases[i];//得到父類 ………… //在dojo聲明的類中都有一個_meta屬性,記錄父類資訊,此處能夠得到包含本身在//內的繼承鏈 lin = base._meta ? base._meta.bases : [base]; top = 0; for(j = lin.length - 1; j >= 0; --j){ //遍曆繼承鏈中的元素,注意,這裡的處理是反向的,即從最底層的開始,一直到鏈的頂端 proto = lin[j].prototype; if(!proto.hasOwnProperty("declaredClass")){ proto.declaredClass = "uniqName_" + (counter++); } name = proto.declaredClass; // nameMap以map的方式記錄了用到的類,不會重複 if(!nameMap.hasOwnProperty(name)){ //每個類都會有這樣一個結構,其中refs特別重要,記錄了引用了依賴類 nameMap[name] = {count: 0, refs: [], cls: lin[j]}; ++clsCount; } rec = nameMap[name]; if(top && top !== rec){ //滿足條件時,意味著當前的類依賴此時top引用的類,即鏈的前一元素 rec.refs.push(top); ++top.count; } top = rec;//top指向當前的類,開始下一迴圈 } ++top.count; roots[0].refs.push(top);//在一個父類處理完成後就將它放在根的引用中 }//到此為止,我們建立了父類元素的依賴關係,以下要正確處理這些關係 while(roots.length){top = roots.pop();//將依賴的類放入結果集中 result.push(top.cls); --clsCount; // optimization: follow a single-linked chain while(refs = top.refs, refs.length == 1){ //若當前類依賴的是一個父類,那處理這個依賴鏈 top = refs[0]; if(!top || --top.count){ //特別注意此時有一個top.count變數,是用來記錄這個類被引用的次數,//如果減一之後,值還大於零,說明後面還有引用,此時不做處理,這也就是//在前面的例子中為什麼不會出現G->E->C->B的原因 top = 0; break; } result.push(top.cls); --clsCount; } if(top){ //若依賴多個分支,則將依賴的類分別放到roots中,這段代碼只有在多繼承,//第一次進入時才會執行 for(i = 0, l = refs.length; i < l; ++i){ top = refs[i]; if(!--top.count){ roots.push(top); } } } } if(clsCount){//如果上面處理完成後,clsCount的值還大於1,那說明出錯了 err("can't build consistent linearization", className); } //構建完繼承鏈後,要標識出真正父類在鏈的什麼位置,就是通過返回數組的第一個元素 base = bases[0]; result[0] = base ? base._meta && base === result[result.length - base._meta.bases.length] ? base._meta.bases.length : 1 : 0; return result; }
通過以上的分析,我們可以看到,這個演算法實現起來相當複雜,如果朋友們對其感興趣,建議按照上文的例子,自己加斷點進行調試分析。dojo的作者使用了不到100行的代碼實現了這樣強大的功能,裡面有很多值得借鑒的設計思想。
3. 鏈式構造器的實現
在第一部分程式碼分析中我們曾經看到過定義建構函式的代碼,如下:
bases[0] = ctor = (chains && chains.constructor === "manual") ? simpleConstructor(bases) : (bases.length == 1 ? singleConstructor(props.constructor, t) : chainedConstructor(bases, t));
這個方法對於理解dojo類機制很重要。從前一篇文章的介紹中,我們瞭解到預設情況下,如果dojo聲明的類存在繼承關係,那麼就會自動調用父類的構造方法,且是按照繼承鏈的順序先調用父類的構造方法,但是從1.4版本開始,dojo提供了手動設定構造方法調用的選項。在以上的代碼中涉及到dojo聲明類的三個方法,如果該類沒有父類,那麼調用的就是singleConstructor,如果有父類的話,那麼預設調用的是chainedConstructor,如果手動設定了構造方法,那麼調用的就是simpleConstructor,要啟動這個選項只需在聲明該類的時候添加chains的constructor聲明即可。
比方說,我們在定義繼承自com.levinzhang.Person的com.levinzhang.Employee類時,可以這樣做:
dojo.declare("com.levinzhang.Employee", com.levinzhang.Person,{ "-chains-": { constructor:"manual" },…………}
添加以上代碼後,在構造com.levinzhang.Employee執行個體時,就不會再調用所有父類的構造方法了,但是此時我們可以使用inherited方法顯式的調用父類方法。
限於篇幅,以上的三個方法不全部介紹,只介紹chainedConstructor的核心實現:
function chainedConstructor(bases, ctorSpecial){ return function(){ //在此之前有一些準備工作,不詳述了 //找到所有的父類,分別調用其構造方法 for(i = l - 1; i >= 0; --i){ f = bases[i]; m = f._meta; f = m ? m.ctor : f;//得到父類的構造方法 if(f){ //通過apply調用父類的方法 f.apply(this, preArgs ? preArgs[i] : a); } } // 請注意在構造方法執行完畢後,會執行名為postscript的方法,而這個方法是//dojo的dijit組件實現的關鍵生命週期方法 f = this.postscript; if(f){ f.apply(this, args); } }; }
4. 調用父類方法的實現
在聲明dojo類的時候,如果想調用父類的方法一般都是通過使用inherited方法來實現,但從1.4版本開始,dojo支援鏈式調用所有父類的方法,並引入了一些AOP的概念。我們將會分別介紹這兩種方式。
1) 通過inherited方式調用父類方法
在上一篇文章中,我們曾經介紹過,通過在類中使用inherited就可以調用到。這裡我們要深入inherited的內部,看一下其實現原理。因為inherited支援調用父類的一般方法和構造方法,兩者略有不同,我們關注調用一般方法的過程。
function inherited(args, a, f){ ………… //在此之前有一些參數的處理 if(name != cname){ // 不是構造方法 if(cache.c !== caller){ //在此之間的一些代碼解決了確定調用者的問題,即確定從什麼位置開始找父類 } //按照順序找父類的同名方法 base = bases[++pos]; if(base){ proto = base.prototype; if(base._meta && proto.hasOwnProperty(name)){ f = proto[name];//找到此方法了 }else{ //如果沒有找到對應的方法將按照繼承鏈依次往前找 opf = op[name]; do{ proto = base.prototype; f = proto[name]; if(f && (base._meta ? proto.hasOwnProperty(name) : f !== opf)){ break; } }while(base = bases[++pos]); // intentional assignment } } f = base && f || op[name]; }else{ //此處是處理調用父類的構造方法 } if(f){ //方法找到後,執行 return a === true ? f : f.apply(this, a || args); }}
2) 鏈式調用父類方法
這是從dojo 1.4版本新加入的功能。如果在執行某個方法時,也想按照一定的順序執行父類的方法,只需在定義類時,在-chains-屬性中加以聲明即可。
dojo.declare("com.levinzhang.Employee", com.levinzhang.Person,{"-chains-": { sayMyself: "before" },……}
添加了以上聲明後,意味著Employee及其所有的子類,在調用sayMyself方法時,都會先調用本身的同名方法,然後再按照繼承鏈依次調用所有父類的同名方法,我們還可以將值“before”替換為“after”,其執行順序將會相反。在-chains-屬性中聲明的方法,在類定義時,會進行特殊處理,正如我們在第一章中看到的那樣:
if(chains){ for(name in chains){ if(proto[name] && typeof chains[name] == "string" && name != cname){ t = proto[name] = chain(name, bases, chains[name] === "after"); t.nom = name; } } }
我們可以看到在-chains-中聲明的方法都進行了替換,換成了chain方法的傳回值,而這個方法也比較簡單,源碼如下:
function chain(name, bases, reversed){ return function(){ var b, m, f, i = 0, step = 1; if(reversed){ //判定順序,即“after”還是“before”,分別對應於迴圈的不同起點和方向 i = bases.length - 1; step = -1; } for(; b = bases[i]; i += step){ //按照順序依次尋找父類 m = b._meta; //找到父類中同名的方法 f = (m ? m.hidden : b.prototype)[name]; if(f){ //依次執行 f.apply(this, arguments); } } }; }
5. 工具方法和屬性如isInstanceOf、declaredClass的實現
除了上面提到的inherited方法以外,dojo在實作類別功能的時候,還實現了一些工具方法和屬性,這裡介紹一個方法isInstanceOf和一個屬性declaredClass。從功能上來說isInstanceOf方法用來判斷一個對象是否為某個類的執行個體,而declaredClass屬性得到的是某個對象所對應聲明類的名字。
function isInstanceOf(cls){ //得到執行個體對象繼承鏈上的所有類 var bases = this.constructor._meta.bases; //遍曆所有的類,看是否與傳進來的類相等 for(var i = 0, l = bases.length; i < l; ++i){ if(bases[i] === cls){ return true; } } return this instanceof cls; }
而declaredClass屬性的實現比較簡單,只是在聲明類的原型上添加了一個屬性而已,類的執行個體對象就可以訪問這個屬性得到其聲明類的名字了。這段代碼在dojo.declare方法中:
if(className){ proto.declaredClass = className; d.setObject(className, ctor); }
在dojo實作類別機制的過程中,有一些內部的方法,是很值得借鑒的如forceNew、safeMixin等,這些方法在實現功能的同時,保證了代碼的高效執行,感興趣的朋友可以進一步的研究。
6. 總結與思考
1) dojo在實作類別機制方面支援多繼承方式,其它JavaScript類庫中很少能做到,而利用JavaScript原生文法實現多繼承也較為困難。在這一點上dojo的類機制的功能確實足夠強大。但是多繼承會增加編碼的難度,對開發人員如何組織類也有更高的要求;
2) 鏈式調用父類方法時,我們可以看到dojo引入了許多AOP的理念,在1.7的版本中,將會有單獨的模組提供AOP相關的支援,我們將會持續關注類似的功能;
3) 在dojo的代碼中,多處都會出現方法替換,如鏈式方法調用、事件綁定等,這種設計思想值得我們關注和學習;
4) 使用了許多的內部屬性,如_meta、bases等,這些中繼資料在實現複雜的類機制中起到了至關重要的作用,在進行源碼分析的時候,我們可以給予關注,如果要實作類別似功能也可以進行借鑒。
探究類庫的實現原理是提高自己編碼水平的好辦法,類似於dojo這樣類庫的核心代碼基本上每一行都有其設計思想在裡面(當然也不可以盲目崇拜),每次閱讀和探索都會有所發現和心得,當然裡面肯定也會有自以為是或謬誤之處,在此很樂意和讀到這篇文章的朋友們一起研究,歡迎批評指正。
參考資料:
http://docs.dojocampus.org/
http://blog.csdn.net/dojotoolkit/
http://dojotoolkit.org/
作者資訊:張衛濱,關注企業級Java開發和RIA技術,個人部落格:http://lengyun3566.iteye.com,微博:http://weibo.com/zhangweibin1981
聲明:
本文已經首發於InfoQ中文站,著作權,原文為《dojo類機制實現原理分析》,如需轉載,請務必附帶本聲明,謝謝。
InfoQ中文站是一個面向中高端技術人員的線上獨立社區,為Java、.NET、Ruby、SOA、敏捷、架構等領域提供及時而有深度的資訊、高端技術大會如QCon 、線下技術交流活動QClub、免費迷你書下載如《架構師》等。