dojo類機制實現原理分析

來源:互聯網
上載者:User

前段時間曾經在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、免費迷你書下載如《架構師》等。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.