写点什么

dojo 类机制实现原理分析

  • 2011-10-26
  • 本文字数:7843 字

    阅读完需:约 26 分钟

前段时间曾经在 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 的概念。我们将会分别介绍这两种方式。

通过 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);
}
}

链式调用父类方法

这是从 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 这样类库的核心代码基本上每一行都有其设计思想在里面(当然也不可以盲目崇拜),每次阅读和探索都会有所发现和心得,当然里面肯定也会有自以为是或谬误之处,在此很乐意和读到这篇文章的朋友们一起研究,欢迎批评指正。

参考资料:

关于作者

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


感谢侯伯薇对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2011-10-26 00:006998

评论

发布
暂无评论
发现更多内容

Ping Exporter -- Prometheus

耳东@Erdong

Prometheus ping 7月日更 exporter

要性能还是要模型?

escray

学习 极客时间 7月日更 如何落地业务建模

Spark SQL和DataFrames:内置数据源简介(四)

Databri_AI

spark sparksq

Discourse 调整使用不同的表情符号

HoneyMoose

2021年网络安全市场全景图,看看都有哪些企业吧!

郑州埃文科技

Liunx之chown命令

入门小站

Linux

模块-1 微信业务架构图 和 备选方案

小遵

如何通过代码审计从三层内网各种漏洞拿到域控?

网络安全学海

黑客 网络安全 信息安全 安全漏洞 渗透测试·

一文搞懂二分查找面试

泽睿

算法 二分查找

golang解析---进程,线程,协程

en

线程 进程 协程 Go 语言 goroutine

有趣的SVG、Favicon

devpoint

favicon SVG 7月日更

架构实战营 模块二 作业

一雄

作业 架构实战营 模块二

终于学完了阿里P8大牛推荐的527页Java性能优化实践文档

Java 编程 架构 面试

hdfs中抽象为block块的好处

五分钟学大数据

hdfs 7月日更

Python 正则表达式急速入门

喵叔

7月日更

模块2作业

Tina

架构实战营第二周作业——微信朋友圈高性能复杂度

发酵的死神

架构实战营

Spark :: 源代码(前传)—Spark多线程 :: NettyRpcEnv.ask解读

dclar

spark 多线程 Spark调优 源代码

JDBC数据库链接的那些事

卢卡多多

JDBC 7月日更

【Flutter 专题】84 图解自定义 ACEWave 波浪 Widget (二)

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 7月日更

分布式事务最经典的七种解决方案

叶东富

数据库 分布式事务 微服务 TCC Go 语言

架构实战营模块二作业

王晓宇

模块-6 拆分电商系统为微服务

小遵

🐧【Linux技术专题系列】「必备基础知识」一起探索(su、sudo等相关身份提权/身份切换机制)

洛神灬殇

Linux linux 文件权限控制 7月日更 Linux身份提权

微服务架构服务容错设计分析

慕枫技术笔记

架构 微服务 后端

我佛了!花重金求来的并发编程笔记,颠覆了我以往“正确“的认知

Java 编程 程序员

昇腾AI的三级跳

脑极体

【通证经济】价值、应用、市场,被掩盖的区块链宝藏!!

CECBC

毕业1年,凭借一份 “漫画” 杀进大厂?他是怎么做到的?原理篇+框架篇

Java架构师迁哥

🐧【Linux技术专题系列】「必备基础知识」一起探索(用户、用户组与文件权限)

洛神灬殇

Linux 操作系统 linux 文件权限控制 7月日更

深入了解Spring之MessageSource

邱学喆

MessageFormat MessageSource MessageSourceAware

dojo类机制实现原理分析_语言 & 开发_张卫滨_InfoQ精选文章