写点什么

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:006953

评论

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

质量分析工具-监控大厅大揭秘

anyRTC开发者

音视频 WebRTC sdk

企业应用AI之路怎么走?飞桨实践有真知

百度大脑

AI 飞桨

百度灵医智惠明星案例获人民日报点赞:智慧医疗让看病更便捷

百度大脑

人工智能 智慧医疗

面试官:如何给字符串设计索引?

一个优秀的废人

MySQL 索引 字符串 索引优化

我人生的里程碑之【作为独立开发者,第一次承接外包项目的心得经历,也许说出你的心声哦!】

洛神灬殇

程序人生 6月日更

小白必看的,JS中循环语句大集合

华为云开发者联盟

JavaScript js 循环语句 while循环 for循环

建信金科大咖访谈:ISO20000及ISO27001标准体系解读

金科优源汇

待办事项列表,敏捷项目管理的核心工件

万事ONES

Scrum 敏捷 研发管理 ONES

博云作为专业独立PaaS厂商,入选中国PaaS市场研究报告

BoCloud博云

PaaS

准备3个月,面试10分钟,Java中高级岗面试为何越来越难?

Java架构师迁哥

油管视频下载: 如何下载油管视频到本地

科技猫

分享 教程 经验 油管视频下载 下载油管视频

Kubernetes学习笔记之Calico CNI Plugin源码解析(二)

360技术

理解Linux之文件I/O——知其然,知其所以然

奔着腾讯去

文件管理 Linux内核 文件I/O I/O模型

构建高可用的MySQL

林一

MySQ MySQL 高可用 Maxscale

基于传感器的人体生命体征监控技术

不脱发的程序猿

物联网 传感器 智能医疗 人体生命体征监控技术

毕昇JDK:为啥是ARM上超好用的JDK

华为云开发者联盟

Java 华为 jdk Openjdk 毕昇 JDK

带你剖析鸿蒙轻内核任务栈的源代码

华为云开发者联盟

鸿蒙 任务栈 任务调度 任务上下文

宜兴牵手百度智能云共建人工智能应用中心,推动数字经济创新发展

百度大脑

人工智能

如何针对美工与设计师的Maya工具进行版本控制

龙智—DevSecOps解决方案

如何科学制定和管理项目计划?

万事ONES

项目管理 ONES 项目经理

程序员需要了解数据库知识么?

escray

学习 极客时间 朱赟的技术管理课 6月日更

react源码解析9.diff算法

全栈潇晨

react源码

从零开始学习3D可视化之模型动画

ThingJS数字孪生引擎

可视化 模型 大屏可视化 数字时代 3D可视化

想做DBA,多租户管理你一定要知道这些

华为云开发者联盟

多租户 GaussDB(DWS) 资源池 存储空间 资源隔离

☕️【Java技术之旅】带你一起探究String类不可变的特性

洛神灬殇

string 原理 字符串 6月日更

24道几乎必问的JVM面试题,我只会7道,你能答出几道?

北游学Java

Java 面试 JVM

阿里云官方出品:全面总结阿里云云原生架构方法论与实践经验

尹文敏

云计算 阿里云 云原生

☕️【Java 技术之旅】带你一起攻克String类创建的难点分析

洛神灬殇

Java string pool string 6月日更

福利时刻 十年黑客大佬的Web安全渗透技术分享

学神来啦

Linux 黑客 安全 运维自动化

Flink + Iceberg 在去哪儿的实时数仓实践

Apache Flink

flink

开源之夏来啦,欢迎报名 Apache APISIX 项目!

API7.ai 技术团队

开源 后端 技术人生 API 网关

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