`
lengyun3566
  • 浏览: 446978 次
  • 性别: Icon_minigender_1
  • 来自: 大连
博客专栏
D59180b9-02f1-3380-840c-ea34da46143c
《Spring Secur...
浏览量:379441
社区版块
存档分类
最新评论

dojo类机制实现原理分析

阅读更多

声明:本文为笔者原创,但首发于InfoQ中文站,详见文末声明

 

 

前段时间曾经在InfoQ中文站上发表文章,介绍了dojo类机制的基本用法。有些朋友在读后希望能够更深入了解这部分的内容,本文将会介绍dojo类机制幕后的知识,其中会涉及到dojo类机制的实现原理并对一些关键方法进行源码分析,当然在此之前希望您能够对JavaScriptdojo的使用有些基本的了解。

         dojo的类机制支持类声明、继承、调用父类方法等功能。dojo在底层实现上是通过操作原型链来实现其类机制的,而在实现继承时采用类式继承的方式。值得一提的是,dojo的类机制允许进行多继承(注意,只有父类列表中的第一个作为真正的父类,其它的都是将其属性以mixin的方法加入到子类的原型链中),为解决多重继承时类方法的顺序问题,dojoJavaScript实现了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 ,要启动这个选项只需在声明该类的时候添加chainsconstructor声明即可。

比方说,我们在定义继承自com.levinzhang.Personcom.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  工具方法和属性如isInstanceOfdeclaredClass的实现

除了上面提到的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实现类机制的过程中,有一些内部的方法,是很值得借鉴的如forceNewsafeMixin等,这些方法在实现功能的同时,保证了代码的高效执行,感兴趣的朋友可以进一步的研究。

6.  总结与思考

1)  dojo在实现类机制方面支持多继承方式,其它JavaScript类库中很少能做到,而利用JavaScript原生语法实现多继承也较为困难。在这一点上dojo的类机制的功能确实足够强大。但是多继承会增加编码的难度,对开发人员如何组织类也有更高的要求;

2)  链式调用父类方法时,我们可以看到dojo引入了许多AOP的理念,在1.7的版本中,将会有单独的模块提供AOP相关的支持,我们将会持续关注类似的功能;

3)  dojo的代码中,多处都会出现方法替换,如链式方法调用、事件绑定等,这种设计思想值得我们关注和学习;

4)  使用了许多的内部属性,如_metabases等,这些元数据在实现复杂的类机制中起到了至关重要的作用,在进行源码分析的时候,我们可以给予关注,如果要实现类似功能也可以进行借鉴。

 

         探究类库的实现原理是提高自己编码水平的好办法,类似于dojo这样类库的核心代码基本上每一行都有其设计思想在里面(当然也不可以盲目崇拜),每次阅读和探索都会有所发现和心得,当然里面肯定也会有自以为是或谬误之处,在此很乐意和读到这篇文章的朋友们一起研究,欢迎批评指正。

 

张卫滨,关注企业级Java开发和RIA技术,个人博客:http://lengyun3566.iteye.com,微博:http://weibo.com/zhangweibin1981

 

参考资料:

http://docs.dojocampus.org/

http://blog.csdn.net/dojotoolkit/

http://dojotoolkit.org/

 

 

 

 

 

 

声明: 
  本文已经首发于InfoQ中文站,版权所有,原文为《dojo类机制实现原理分析》,如需转载,请务必附带本声明,谢谢。 
  InfoQ中文站是一个面向中高端技术人员的在线独立社区,为Java、.NET、Ruby、SOA、敏捷、架构等领域提供及时而有深度的资讯、高端技术大会如QCon 、线下技术交流活动QClub、免费迷你书下载如《架构师》等。

分享到:
评论

相关推荐

    dojo的包加载机制

    dojo的包加载机制, 源代码中加了注释和debug,只供大家参考

    dojo dojo实例 dojo例子 dojo资料 dojo项目 dojo实战 dojo模块 dojo编程

    dojo dojo实例 dojo例子 dojo资料 dojo项目 dojo实战 dojo模块 dojo编程

    AJAX之Dojo实现登陆框

    AJAX之Dojo实现登陆框

    DOJO TableContainer实现表单布局.js

    DOJO TableContainer实现表单布局.js

    dojo enhancedGrid pagination 分页实现

    利用dojo的enhancedGrid实现分页,利用静态数据加载和json文件方式分别加载,也利用XHR方式获取json文件数据生成grid,另外利用fetch实现分页的过滤、排序等功能。

    DOJO 学习笔记 dojo

    一、 Dojo学习笔记(1. 模块与包) 1 二、 Dojo学习笔记(2. djConfig解说) 4 三、 Dojo学习笔记(3. Dojo的基础对象和方法) 6 四、 Dojo学习笔记(4. dojo.string & dojo.lang) 9 五、 Dojo学习笔记(5. dojo.lang.array ...

    dojo文档 dojo文档 dojo文档

    dojo文档 dojo文档 dojo文档 dojo文档 dojo文档 dojo文档 dojo文档 dojo文档 dojo文档 dojo文档 dojo文档 dojo文档

    dojo实现登陆框架

    dojo 实现登陆框架 浪曦的那个谁讲的我忘了饿

    dojo js dojo js

    dojo js dojo js dojo js dojo js dojo js dojo js dojo js

    精通Dojo by Dojo之父

    Dojo是一个非常强大的、面向对象的、开源的JavaScript工具箱,它为开发富客户端Ajax...学习完本书后,读者不仅能熟悉Dojo框架的使用,还能了解Ajax框架的工作原理,最重要的是,还能随心所欲地开发出自己的RIA应用。

    DOJO中文手册【出自dojo中国】

    Dojo 是一个用javascript语言实现的开源DHTML工具包。它是在几个项目捐助基础上建立起来的(nWidgets, Burstlib, f(m)), 这也是为什么叫它a \"unified\" toolkit的原因。Dojo的目标是解决开发DHTML应用程序遇到的那些...

    定义自己的dojo组件类

    该文档教大家如何定义自己的dojo组件类,并在程序中如何引用

    struts2+dojo实现例子

    struts2+dojo ajax实现小例子,部署后可直接使用,可供学习和参考。

    dojo精品中文教程(包一)

    很不错的中文教程!文件太大分3个包! 目录如下: dojo精品中文教程 Dojo.1.0 Practice Note [1] 什么是dojo 选择dojo的理由 AJAX架构之Dojo篇 Adding Ajax中文版 (DoJo) ...利用Dojo实现拖动(Drag and Drop)效果

    dojo精品中文教程(全)

    分三个包上传时,第三个包好像传不上去,我给整合了一下,打在一个包里上传了! dojo精品中文教程 Dojo.1.0 Practice Note [1] 什么是dojo 选择dojo的理由 ...利用Dojo实现拖动(Drag and Drop)效果

    javascript dojo

    学习dojo的绝好资源学习dojo的绝好资源学习dojo的绝好资源学习dojo的绝好资源学习dojo的绝好资源学习dojo的绝好资源学习dojo的绝好资源学习dojo的绝好资源学习dojo的绝好资源学习dojo的绝好资源学习dojo的绝好资源...

    Dojo.js核心dojo的javaScript类库Dojo.js核心dojo的javaScript类库Dojo.js核心dojo的javaScript类库

    dojo.js.核心jsDojo.js核心dojo的javaScript类库Dojo.js核心dojo的javaScript类库Dojo.js核心dojo的javaScript类库Dojo.js核心dojo的javaScript类库

    dojo入门系列教程.rar

    dojo入门系列教程,包含入门简介,在javascript基础上介绍dojo的语法特色,ajax的dojo包装---xhr框架的编程要点, Dojo 事件机制.以及对dojo最具特色的web UI设计的全面介绍.

    dojo精品中文教程(包二)

    很不错的中文教程!文件太大分3个包! 目录如下: dojo精品中文教程 Dojo.1.0 Practice Note [1] 什么是dojo 选择dojo的理由 AJAX架构之Dojo篇 Adding Ajax中文版 (DoJo) ...利用Dojo实现拖动(Drag and Drop)效果

Global site tag (gtag.js) - Google Analytics