原文地址:http://ghsky.com/2010/09/kissy-research-part-one-kissy.html
看kissy源码不少时间了,有好多地方不明白,今天终于找到一篇文章分析Kissy源码,文章的作者真是厉害啊,分析的真好,看完后真有醍醐灌顶的感觉。感激之心不知怎么表述,贴上语文,顿悟的地方都用红色标注。
?
==============原文 开始===================
?
本文着重从KISSY源代码研究,了解KISSY的整体组成,力求从原理上了解KISSY。笔者本人也是边看源代码边进行分析,做了一下笔记,以供大家参考交流,如果有任何不足和错误之处,希望大家提出来学习讨论!
如果你也clone过一份KISSY的源代码,那么从文件组织上面,src文件下的均是源代码文件,依照模块进行划分,这也是KISSY的代码组织的基本方式――模块。在src目录下,又有一个kissy文件夹,里面包含kissy.js,相比肯定是KISSY的核心文件,那今天就先从它下手吧。
首先我们整体看下kissy.js里面都有什么,如下图,对于其中的方法和属性我已经大致做了分类:
首先,KISSY也是要做到对全局对象最小的污染,因此只暴露了一个接口到全局对象中――KISSY,任何KISSY的模块都依附在这个对象上。同时KISSY只提供了弱沙箱的支持,也就是我们直接使用的就是KISSY这个对象,而不是它的一个实例,也就是对于KISSY对象的任何修改都会在全局内产生影响,简单点儿说,例如有KISSY.DOM这样一个DOM相关的模块,但是如果我在自己的代码中不小心将KISSY.DOM = null,这样置空了之后,那么可想而知,后面任何和DOM相关的方法都会报错,这就是弱沙箱的含义。相反,如果我们想像KISSY这是一个类,每次使用前我们需要实例化出一个实例来使用,例如:var ks = new KISSY();,那么ks这个对象相当于和KISSY这个类产生了隔离,我在ks上的任何操作都不会影响KISSY这个全局的类,这就是所谓的强沙箱,YUI3的设计理念正式如此。但 由于KISSY的愿景一样:“小巧灵活,简洁实用,使用起来让人感觉愉悦”,弱沙箱的设置正是权衡之后的提现,在后续的代码分析中,我们会逐渐感受到KISSY这种实用主义的设计理念。
贯穿在kissy.js中的一个最基本的函数就是mix(r, s, ov, wl),其最基本的作用是将一个对象的成员拷贝到另一个对象中,这里的拷贝指的是浅拷贝。这里函数参数的r指的是receiver,也就是拷贝接收对象,s指的是supplier,拷贝的对象提供者。通过ov参数(指的是override)可以将接收者中与提供者重名的属性(方法)覆盖,wl参数(指的是wantlistwhitelist)可以只拷贝列表中指定的属性(方法)。mix函数的逻辑清楚之后,实现起来应该也不是什么问题,这里就不多说了。
源代码中,全部的kissy.js代码都是放到一个(function() {})();匿名函数的闭包中,这就是一个很简单的沙箱,保证了其中的变量不会溢出到全局作用域中,同时我们注意到你们函数的参数列表传入了win, S, undefined几个参数,其中win是window对象,S是'KISSY'字符串,也就是我们暴露到全局对象中的唯一接口的名称,undefined参数实际作用就是指代undefined,这样写的好处是,我们在调用你们函数的时候没有给undefined参数传值,那么其值就是undefined,由于undefined是一个关键字,代码压缩的时候是不能够压缩的,因此这里用参数undefined这个变量(注意,这时候undefined已经是一个变量了而不再是一个值),来代替undefined值,代码压缩的时候自然就可以将这个参数给放心的压缩了,而不用考虑到其是一个关键字而不能压缩,这实际是一个小技巧,保证代码压缩的时候能够尽最大可能被缩短,后面定义的各种变量,很多变量在一定程度也是为了达到提高压缩率的作用,比如刚刚提到的win参数,后面定义的doc = win['document'], loc = location类似的都是为了达到提供代码压缩率。同时对于对象访问,不仅可以提高代码压缩率,同时也可以提高访问效率。我们还注意到一些常量的定义,其基本考虑也是提供代码压缩率,在大量出现常量的地方,使用一个变量存储,之后代码的压缩率可以提高很多。关于JavaScript代码压缩这块的文章,可以Google一下,或者看下这个Slide。
我们整体看下kissy.js里面的代码组织,首先我们有了一个全局的接口对象S(window['KISSY']的一个快捷方式),然后我们有一个mix方法,现在我们便可以很方便地将各种属性、方法mix到S上面,这样组织的好处就是我们不必纠结到底要把这些代码放到哪个对象上,因为只用简单地修改一下mix的参数,便能将代码很容易的转移到其他对象上。那现在我们需要往KISSY这个对象上mix哪些东西呢?首先最基本的,版本信息,类库的配置信息(主要是是否开启debug模式,以及load加载相关参数),还有一个最重要的是类库的模块存储。既然我们这前说过KISSY代码最基本的组织方式是以模块方式结合,比如有DOM模块,Event模块,Node模块等系统提供的模块,还有一些用户自定义添加的模板,那么他们都需要存储,那存储到哪里比较合适呢?很自然的想法就是在KISSY对象上开辟一个对象来存储他们,这里便是Env对象的作用,里面的mods存放了各种添加的模块,而_loadQueue则用于load机制的控制。有关模块的设计理念,组织和加载等,我们在后续讲到KISSY的load机制时再详细介绍。
现在类库相关的东西都存储好了,剩下的就是一大堆方法。其中直接依附到KISSY这个对象上的方法一定,其作用一定要是全局的,当然也是最重要的,他们主要包括这么几类:类库、子类库(应用)代码的组织工具,类库加载、调用工具,语言工具(JavaScript语言增强、修补),类库调试工具,这些工具最终决定了整个类库的代码组织方式,子类库组织方式,OO风格,类库合适加载、执行,类库调试等方式。但是现在我们仅仅考虑kissy.js,实际上我们可以发现,在kissy目录下的所有代码,最终实际都是直接附加到KISSY对象上,他们可以构成整个KISSY类库的最核心部分,实际上有了这个core,我们便可以用其方便地创建各种子类库或者应用,而实际其他模块加入到其中,简单一点说只是为了方面我们创建各种与浏览器相关的东东,因此core就像一个设计精巧的机器人(攻城师),事在人为嘛,其实有人就够了,但是毕竟需求那么多、那么强大,我们只好给这个机器人进行全副武装(往其中添加各种模块),最后我们便能完成“攻城”,利用这些利器攻克一个个顽固的需求。
继续往下分析,我们会注意到一个ready方法,他的本质就是在DOM加载完成后触发的一个回调,按照我们刚刚的理论,实际上DOM相关的方法因为是按模块方式加入到KISSY中,core应该尽量保持简洁,那为什么这里要在core中直接出现一个DOM相关的方法呢?我们可以把脚本加载启动想像为一台电脑启动,我们core的作用就相当于一个引导程序的作用,提供了最基本的工具引导主要脚本加载、使用,而何时能够开始启动其他程序这就是DOM ready的作用,因为其不受业务脚本的影响,只于页面内容本身有关,因而起到了在DOM ready后,引导业务脚本执行的作用,因此把ready方法放在core中是考虑到脚本的实际使用情况。对于ready这个方法,考虑到一定会被多次调用,DOM ready前需要把注册了的所有函数保留起来,DOM ready之后,就需要依次执行所有注册过的函数即可。因此需要设置两个状态变量,一个用于记录DOM ready事件是否已经注册监听,另一个需要记录是否已经DOM ready,同时还需要一个队列保存所有需要执行的函数。对于注册DOM ready事件,兼容性是一个比较棘手的问题,这里推荐几篇文章模拟兼容性的 addDOMLoadEvent 事件,YUI 中 onDOMReady 的 iframe bug,在kissy.js里面分别用_bindReady方法和_fireReady方法作为内部调用,来注册DOM ready事件已经DOM ready后触发函数调用。和ready方法类似,只不过available方法是用于检测元素是否可用,原理也很简单,可以反复调用getElementById函数,检测元素是否找到即可判断是否可用了,只是需要保证一定的测试延迟以及探测次数。
对于类库,同样需要提供构建构建OO代码的最基本方法,最开始说到的mix方法可以视作之一,由于JavaScript语言的特殊性,其OO方式也与其他传统语言的OO不一样,merge,augment,extend三个方法都或多或少的用到了mix,其中merge和augment两个方法可以视作mix的特殊定制、改良版本。merge,顾名思义,将参数列表中的元素合并,并且满足后向前覆盖的原则,返回一个合并后的对象,同时如果参数只有一个对象的话,就相当于做了一个简单的浅拷贝。augment方法是将提供者的prototype对象(或者自身)合并到接收者的prototype上,正如其名字一样的,扩大接收者的prototype对象。extend方法则用来提供较为完善的继承机制,包括继承父类的prototype,修复自己prototype的constructor,添加superclass属性用于向上访问到父类,同时可以提供参数用于覆盖prototype上的方法(属性),或者覆盖类方法(属性)(这里的类方法可以理解为静态方法,即直接写于构造函数上的方法(属性))。
基于类库需要构造相关的应用,就需要考虑到命名空间的问题,kissy.js里面提供了namespace和app两个方法来解决这个问题。首先namespace方法是用于返回一个指定命名空间用于存放东西的,而app方法则会基于KISSY对象,拷贝指定方法,返回一个新的对象,里面包含KISSY的基本配置以及完善的模块、load机制,这里的app主要是用来组织一个应用级别的代码体系,由于包含完整的模块和load机制,每个应用都可以有自己的模块,这就相当于就有较大的可自定义性,而不用考虑KISSY自身的模块。正如现在淘宝的组织方式,首页就是一个app,包含了各个模块,这样就可以基于首页这个独立应用来考虑模块的组织,而不用担心将首页模块添加到KISSY模块中产生的混乱。
最后还有两个简单的有关类库的调试工具,这里就不详细介绍了。
现在已经对kissy.js中附加到KISSY对象上的方法有了大致的了解,最后KISSY对象的初始化配置函数还是需要再提一下,这个方法叫__init,也是直接依附与KISSY对象上,从命名上就可知道是仅供内部使用的,这个方法主要是做什么事儿呢?首先是要初始化KISSY对象的模块容器,同时设置相关的配置属性,这里的base主要是用于模块加载时候使用的,它的值就是当前KISSY执行脚本的地址,之后各个模块就可以通过这个地址进行定位,获取加载路径,这样加载起来就很智能,只需要部署的时候按照目录放置文件,后续的模块加载就没问题。
现在提及了很多模块的有关概念,下次我们会重点了解一下KISSY的模块机制以及加载方式,从中大致了解KISSY的设计理念。