【AICon】探索RAG 技术在实际应用中遇到的挑战及应对策略!AICon精华内容已上线73%>>> 了解详情
写点什么

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

评论

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

JVM入门,认识Class文件

Simon郎

JVM Java 分布式

架构师Week5总结

lggl

总结

天啊!怎么会有人把Spring Cloud微服务架构讲得这么透彻?

Java架构之路

Java 程序员 架构 面试 编程语言

大四女学霸社招竟成功签约字节跳动,拿下30万年薪?

Java架构师迁哥

KubeVela 正式开源:一个高可扩展的云原生应用平台与核心引擎

阿里巴巴云原生

阿里云 开源 Kubernetes 云原生 OAM

年轻人不讲武德不仅白piao接口测试知识还白piao接口测试工具会员

测试人生路

接口测试

胡继晔:中国应建区块链行业准入制度

CECBC

区块链 金融 数字经济

基于SpringBoot、SpringCloud、Docker微服务架构实战,资源分享

Java架构之路

Java 程序员 架构 面试 编程语言

某美团程序员爆料:筛选简历时,用go语言的基本不看!网友:当韭菜还当出优越感了!

Java架构师迁哥

《迅雷链精品课》第五课:账户与账本

迅雷链

区块链

区块链,音乐,流媒体和版税

CECBC

区块链 艺术

太赞了!腾讯T3-3架构师整理了5000页的Java学习手册免费开放下载

Java架构之路

Java 程序员 架构 面试 编程语言

云原生2.0时代下,DevOps实践如何才能更加高效敏捷?

华为云开发者联盟

云计算 数字化 华为云

小学妹问我:如何利用可视化工具排查问题?

田维常

可视化

【涂鸦物联网足迹】涂鸦云平台消息服务—顺带Pulsar简单介绍

IoT云工坊

人工智能 物联网 云服务 Apache Pulsar 云平台

分布式事务太繁琐?官方推荐Atomikos,5分钟帮你搞定

互联网应用架构

分布式事务 springboot

科普干货|漫谈鸿蒙LiteOS-M与HUAWEI LiteOS内核的几大不同

华为云开发者联盟

华为 鸿蒙 IoT

SpringBoot:整合Swagger3.0与RESTful接口整合返回值(2020最新最易懂)

比伯

Java 编程 架构 面试 计算机

一次 Java 进程 OOM 的排查分析(glibc 篇)

996小迁

Java 编程 架构 面试 计算机

区块链在债券市场如何应用

CECBC

区块链 债券

收藏!数据建模最全知识体系解读

华为云开发者联盟

数据仓库 数据 数据建模

开个交易所需要多少费用?数字货币交易所搭建

13530558032

架构师Week5作业

lggl

作业

LAXCUS大数据集群操作系统挖矿

陈泽云

大数据 分布式计算 挖矿

高性能利器!华为云MRS ClickHouse重磅推出!

华为云开发者联盟

数据库 Clickhouse MRS

Forrester 最新报告:阿里云稳居领导者地位,引领云原生开发浪潮

阿里巴巴云原生

阿里云 Serverless Kubernetes 容器 云原生

《Python程序员面试算法宝典》PDF 超清版免费领取

计算机与AI

Python 面试 算法

《垃圾回收的算法与实现》.pdf

田维常

垃圾回收

区块链数字货币钱包源码价格,区块链多币种钱包

13530558032

云算力矿机源码价格,区块链挖矿平台开发

13530558032

【云图说】第189期 初识数据仓库服务

华为云开发者联盟

数据库 数据仓库 数据

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