KISSY深入研究(3)——loader.js

由于国庆大假的原因,加之最近工作上时间比较零散,没有相对较为完整的时间学习,因此KISSY源码分析停滞了很久,今天要分析的是在KISSY 1.1.5中才正式引入的loader概念。

Loader 背景

首先需要了解一些JavaScript Loader的相关知识,Loader这个概念主要是出现于页面加载时对于JavaScript代码的加载管理、依赖关系处理等。如果本身页面较为简单,可能Loader就并不适用于此。豆瓣的这篇文章《攻城利器之微型框架 - Do》正是介绍了它们的微型框架对于页面JavaScript代码的加载管理,这个框架的主要用途并不像普通JavaScript框架,它的主要用途是页面中JavaScript代码的管理者和组织者,具体介绍可以详细看原文已经整个框架代码。同时还有这篇文章《KISSY loader 的设计 》,是KISSY loader机制的开发者之一@拔赤介绍的相关设计理念,强烈推荐仔细阅读。

Loader 使用

有了对于Loader的基本概念以及相关的设计理念后,我们先从比较直观的方面介绍一下Loader。有关Loader的具体API文档可以猛击这里

我们从一个实例具体理解一下Loader的使用吧。在淘宝网首页中存在如下一段JavaScript代码:

<script src="http://a.tbcdn.cn/??s/kissy/1.1.3/kissy-min.js,p/header/header-v8-min.js?t=20100908.js"></script> 
<script> 
KISSY.app('FP');
FP.add({
    'direct-promo': {
        fullpath: 'http://a.tbcdn.cn/p/fp/2010c/js/fp-direct-promo-min.js'
    },
    'fp-mods': {
        fullpath: 'http://a.tbcdn.cn/??s/kissy/1.1.3/suggest/suggest-pkg-min.js,s/kissy/1.1.3/datalazyload/datalazyload-pkg-min.js,s/kissy/1.1.3/switchable/switchable-pkg-min.js,s/kissy/1.1.3/flash/flash-pkg-min.js,p/fp/2010c/js/fp-alimama-ecpm-min.js,p/fp/2010c/js/fp-alimama-ecpm-min.js,p/fp/2010c/js/fp-hubble-monitor-min.js,p/fp/2010c/js/fp-p4p-min.js,p/fp/2010c/js/fp-init-min.js?t=20100915.js'
    }
});
FP.use('direct-promo', function(F) {
    F.DirectPromo.request([48, 49, 75, 51, 52, 53, 68, 73, 74], 1);
});
</script> 

可以很清楚地看出这里使用了Loader技术。根据前面学习到的知识,在第二段JavaScript代码块中第一行首先是创建了一个FP的应用。之后我们往FP这个应用上添加了两个模块'direct-promo'以及'fp-mods',而且这两个模块的添加方式都是使用文件引用,当然我们也可以直接将代码写在这里,同时还可以加上一些配置,具体写法还是参照API文档的说明。接下来就开始在FP这个应用是使用我们添加的模块做事儿了,这里指的就是use这个方法。

那么这种写法(用法)有什么好处呢?与最原始的方式又有什么不同呢?我想通过背景里面的两篇文章大家一定都能有一些概念了,下面我们看下这样加载方式的Timeline

taobao-index-loader

是的,我们只需预先把种子脚本载入(种子脚本相对较小,但是因为首页具体情况,这里没有使用seed.js种子脚本),然后其后再按需add模块代码,use模块,就能保证我们这里的业务脚本无阻加载,这样不仅仅使代码管理起来更加集中、规范,同时也能大大提高页面性能。有时我们没办法做到不加载,但是通过延时加载,合理分配任务,让繁重工作分时完成,同样能够达到提高页面性能、用户响应的目的,增强网站可访问性。

Loader 概述

既然已经对Loader有了大致了解,并且通过一个例子也算是比较直观地感受了Loader带来的好处,那么我们下面就深入到loader.js源码中具体研究研究这种设计是如何实现的。

我们用一段典型代码来说明整个Loader 的实现过程:

// Add mod1 module for KISSY
KISSY.add('mod1', {
    fullpath: 'http://ghsky.com/labs/foo.js'
});

// Add mod2 module for KISSY
KISSY.add('mod2', function() {
    // some code here
}, {
    requires: ['mod1']
});

// Use the mod2 to do something
KISSY.use('mod2', function() {
    // callback here
});

对于模块主要分为add以及use两个阶段,add阶段主要是添加模块相关配置信息到模块宿主环境中保存,以下是一张典型的宿主环境的模块存储结构:

KISSY.Env

同时添加模块的方式主要有两种,一种是直接以代码形式添加(代码模块),例如上面示例代码的'mod2',另外一种就是以文件资源形式添加(资源模块),例如上面示例代码的'mod1'。由此我们引出模块的5种主要状态,0(初始状态),LOADING(1),LOADED(2),ERROR(3),ATTACHED(4)。

0和LOADING状态只有资源模块会存在,而所有的代码的初始状态均为LOADED。处于LOADED状态的模块均表示外部资源已经读取或者不需要外部资源,此时与ATTACHED状态的唯一差别就是模块自身的代码栈未执行,这里提到模块自身的代码栈,主要指的还是代码模块,当然有些资源模块添加之后同样还可以为其添加一些代码,这样也就成为他们自身的代码栈了。只有当每个模块的依赖模块都加载完毕才会执行其自身代码栈,当然这些工作都完成后,模块也就是ATTACHED(可用)了!

可能你还注意到宿主环境里面还有一个_loadQueue的对象,其作用主要是用于保存资源文件的加载队列,后面会详细介绍。下面我们就大致看一下Loader模块的具体组成:

loader

暴露的公共API就是三个,getScript, add, use,三个方法的具体说明可以猛击这里查看。其他的方法均为内部使用,因此才去了私有方法的命名方式,这里先说一下,__mixMod & __mixMods 两个方法设计到了一些全局loader的高级的特性,因此这里就不具体研究。下面我们主要从add以及use两个方法入手来具体了解loader的实现机制。

Loader add()

正如上面示例所示,add方法仅为宿主环境添加所需的执行代码(或者资源文件)以及模块的配置信息,添加之后的模块只有在use时才能体现出其作用,因此要注意理解add与use的区别。首先大致了解一下add方法的处理流程:

Loader.add

具体解释一下上述的流程,add首先根据传递的参数进行规格化处理,然后判断添加的模块类型(代码模块或是资源模块)。若是资源模块,则很简单,目前只需简单地将模块的相关配置信息规格化后加入到宿主模块环境中;若是代码模块,则首先也要进行相关配置的规格化处理,之后需要判断模块是否有依赖模块,若有的话则检查各个依赖模块是否均已ATTACHED,若是的话就可以执行自身的代码栈,否则需要将代码压入代码栈等待后续执行,至此整个add过程就已经结束了。这里主要涉及到了__isAttached以及__attachMod两个方法。

__isAttached方法顾名思义,就是检查模块是否已经可用了(ATTACHED),其判断依据便是每个模块均有的状态标记,之前的“概述”里面已经提到了模块的状态。

__attachMod方法则是attach模块,也就是执行模块自身代码栈的方法。该方法的执行条件是依赖模块均已可用,这时候便可以执行模块自身的代码栈,顺利执行之后即可将模块的状态设置为ATTACHED(可用)。因此__attachMod方法可以保证代码模块是在依赖模块加载后才执行,因此可以保证代码执行安全。

正如示例所示,我们以外部资源方式添加了'mod1',以代码形式添加了'mod2',此时add方法对于'mod1',只简单规格化配置后就将其添加至宿主模块环境中,只是目前其状态字段为undefined,其所需的外部资源也未读取。对于'mod2',以代码形式添加,但是由于其依赖模块'mod1'在__isAttached检测中未通过,也就是'mod2'的代码还暂时无法执行,故只能将其压入其代码栈等待后续执行。

Loader use()

use方法相对add就要复杂一些,下面先大致看下基本流程:

Loader.use

这么一看大致流程估计也够晕的,那么下面就顺序解释一下。首先声明一下,use的参数中可以传递一个global的参数,但由于其使用较少,笔者基本没有使用经验,所以这里就避而不谈了。use方法支持同时加载多个模块,其方式是以“,”分隔各模块名。方法首先将需要use的模块建立成数组形式,然后调用__isAttached方法检查是否均已可用,若都加载完毕可用,那么就直接执行回调函数退出即可。对于只要存在一个不可用的模块,则比较对其加载,因此需要遍历这个模块数组,排除那些已经可用的模块,只用处理还未可用的模块,同时use方法支持按序执行,也就是如果需要use多个模块,且需要按序加载他们,这里use方法便会利用一个小技巧,将当前模块之前的一个模块(也就是保证按序加载)放入到当前模块的依赖中,这样由于依赖模块均在当前模块前加载可用,因此变相实现了按序加载,对于这种方式会破坏原始的依赖模块数组因此需要做一个备份,当前模块加载可用后再做一次还原即可。同时按序加载中还需要防止循环依赖的情况出现。

目前已经将当前模块的配置处理完毕,包括可能出现的按序加载处理,之后便需要调用__attach方法加载当前模块。这个方法也是利用回调机制,执行完毕后回调处理函数。我们这里的回调处理函数实际就相当于要执行use方法的回调,只是这里需要保证的是整个use列表的模块都已经加载完毕之后才可执行,因此这里需要简单的判断。进入__attach方法,如果存在依赖模块则需要递归地先对依赖模块加载,之后如果当前模块为资源模块的话便需要处理资源加载的问题,首先需要建立资源的绝对路径(处理模块配置中使用的相对路径,同时这里如果存在模块的样式资源文件引用,也需要处理其加载路径),完成后在模块上形成一个'fullpath'(可能还存在'csspath')这样的属性指定加载资源的绝对路径。资源文件路径都准备好了之后就需要进入__load方法,读取模块资源。

在__load方法中,还记得我们最开始说到的在KISSY这个宿主环境中有一个_loadQueue的对象么,这里我们用其来缓存每一个需要加载的外部资源。首先获取_loadQueue中当前资源的信息,然后初始化当前模块的状态(注意对于那些代码模块,在add方法中我们已经初始化过其模块状态为LOADED,这里便不会覆盖),同样对于已经存在于_loadQueue中的资源同样,判断其是正在加载或者已经加载完毕来初始化当前模块的状态。如果存在css资源加载的话,直接通过getScript方法加载资源即可,同样,对于那些还未加载过的资源,设置模块状态为LOADING,然后使用getScript进行加载并注册相关回调来修改模块状态,对于那些已经被之前模块申请加载中的模块,只需要再注册上一个回调函数即可。内嵌代码则直接执行__attach中注册的回调函数。

loader_callbacks

刚刚提到的几个方法都存在比较复杂的回调关系,从上图可以重新理清一遍思路。这个use方法使用回调机制于包括加载模块、读取资源、资源载入几个部分有机地联系起来。

最后再啰嗦一下,前面我们一直提到宿主环境这个词,前面也讲到多次,那么到底什么算一个宿主环境呢?其实简单说就是KISSY以及用KISSY创建的APP都算一个宿主环境,在其上都存在add, use等方法,且对于各个宿主环境的模块是彼此独立的,因此在宿主环境的模块对象中是彼此相互不同的,但是对于_loadQueue这个对象比较特殊,因为__load方法对其的操作均是在KISSY这个宿主环境中,而非其衍生的宿主环境。为什么这么处理呢?因为刚刚提到每个宿主环境的模块是彼此独立管理的,但是可能某些不同的模块使用了同样的外部JavaScript资源,那么如果我们不用_loadQueue进行管理便可会导致外部JavaScript资源重复载入两遍,因为JavaScript每次载入均会执行,因此可能导致未知异常,需要将其异步载入的节点加入_loadQueue中进行状态判定,但是对于CSS样式资源文件变不会存在这种执行问题且其载入也是同步的,因此不用在_loadQueue中保存载入的节点,只需记录其读取状态为LOADED即可(当然若此时由于一些未知因此导致的载入失败,状态也是无法判定的)。

getScript方法是典型的异步脚本载入方式,实现也是很简单的,同时其还支持CSS资源的载入,原理也是大同小异,这里也就不赘述了。

结束

至此从Loader机制的设计理念到具体实现方式已经大致介绍完毕,从篇幅上便可知其复杂程度,主要是由于其函数间依赖关系比较复杂,因此需要理清思路才能顺利理解。其实很多问题也都是在实现或者使用过程中才遇到的,从源代码中可见开发者是基于实际开发经验以及众多类库相关机制理解后才能够实现得如此周密。因此对于我们个人学习或练习实践中大可在思路理清之后形成一个大致程序框架,明确相关关系,而具体可能的细节问题不用在构架中过于纠结,逐渐尝试和调整,总之不可能一步登天,做到如此周详的考虑。

- EOF -

KISSY深入研究(2)——dom-data.js

概述

dom-data子模块隶属DOM模块下,其作用与jQuery中的数据缓存类似,主要是用于在元素上存储数据,其特点是能避免传统方式中因循环引用而引起的内存泄漏风险,同时还可以处理对于全局对象附加数据,限制对全局对象污染的情况。

缘由

之于概述里面所提到内存泄漏如何理解呢?假设我们有以下代码:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Data</title>
</head>
<body>
    <a href="#Foo" onclick="showInfo(this); return false;">I'm Foo!</a>
    <a href="#Bar" onclick="showInfo(this); return false;">I'm Bar!</a>
    <script>
        (function() {
            var peopleDataBase = [
                {
                    'name': 'Foo',
                    'age': 28,
                    'sex': 'male'
                },
                {
                    'name': 'Bar',
                    'age': 19,
                    'sex': 'female'
                }
            ], people = document.getElementsByTagName('a'), i = 0, len = people.length;
            for (; i < len; ++i) {
                peopleDataBase[i]['ref'] = people[i];
                people[i].data = peopleDataBase[i];
            }
        })();
        function showInfo(self) {
            var data = self.data;
            alert('Name: ' + data.name + '\nAge: ' + data.age + '\nSex: ' + data.sex + '\nMsg: ' + data.ref.innerHTML);
        }
    </script>
</body>
</html>

实例目的很简单,每个链接上绑定了数据库中的数据条目,后续会使用到其上的数据,且数据中的ref字段指定了这条数据绑定于哪个链接上。这个简单的例子就会引起内存泄漏,原因是由于链接上直接与数据库中的数据条目做了绑定,同时我们为了在数据库条目中知道这个条目是在何处引用,因此ref字段又指向了这个链接,因此就形成一个循环引用,导致内存泄漏。

memory_leak

这里对于内存泄漏的讨论还是很初步的仅仅包含对象相互引用导致的问题,但是事件注册,闭包使用也会导致内存泄漏的情况,具体可以猛击IE 内存泄露问题,其中包含比较丰富的讨论。

对于概述中提到的第二种情况,也就是全局对象的数据绑定,相信大家都有概念就是尽量不要污染全局对象,尽可能做到无侵入,但是对于众多必须使用的全局数据如何处理呢?那么使用数据缓存的方式同样也能解决。

那么除去一、二种情况的其他情况呢?自然就是普通对象的数据绑定,但是对于这种情况的应用场景实在不常见,这是这里提出一种理念,是处理全局对象数据绑定时候可以借鉴的。

本质

刚刚上述的数据绑定实际上是涉及到了JavaScript中的一个expando的概念,expando可以叫做可扩展对象,主要是动态语言所具有的一个语言特性,即可在运行时动态地增、删、替换其上的任何成员,这与传统静态语言,只能在设计阶段明确声明成员或行为然后编译,有着本质的差别,如果想要更多地了解expando,请猛击这里。那么数据绑定,本质就是对需要绑定数据的元素的expando操作,只是在处理这些元素的expando时,需要考虑到几种情况。

原理

既然直接往元素上附加数据很容易导致内存泄漏,全局对象的数据绑定也有较大风险,那么如何解决这些问题呢?下面就隆重推出KISSY中的数据绑定,KISSY.DOM中data相关方法的原理介绍,方法的具体使用说明可以参看API文档

KISSY.DOM.data方法是数据绑定中的核心方法,遵循KISSY的API设计规范,这个方法同样包含getter和setter两种模式,通过传入参数的个数不同进行操作。这里的数据绑定方式和众多类库相似,使用了缓存机制,当然,与我们之前研究过的KISSY事件模型(1)(2)(3)也有异曲同工之妙。正是基于防止内存泄漏的考虑,我们仅在元素对象本身上附加简单字符串,通过其与缓存中匹配,提取到真正需要的数据,这样就不会产生对象循环引用的潜在威胁。同时在处理全局对象的数据绑定,我们也可以考虑仅仅在全局对象中附加这样的简单字符串,使用时同样通过数据缓存获取(设置)真正的数据,这样我们仅仅会有一个变量侵入全局对象。综上所述,我们可以把这里提到的缓存分为三大类处理,一类就是Node节点的数据缓存,另一类是全局对象的数据缓存,最后一类就是普通对象数据缓存。

Node节点数据缓存的结构如下图所示:

data_cache

对于Node节点数据缓存,我们采用闭包中的一个局部变量作为缓存变量,这样保证了缓存只可由特定的方法提供访问、修改。那么Node节点与缓存中数据相连接的桥梁就是expando,其key是以“_ks_data_”开头的每个KISSY库唯一的值,value就是每个元素所具有的唯一id。这里的key仅仅是节点建立与缓存的桥梁,实际用于在缓存中寻找数据的每个节点唯一的id,在缓存中检索数据时,它便成为索引的key。正是由于这里的expando是简单的字符串,因此不存在对象循环引用的问题,从而避免了内存泄漏的危险。

全局对象数据缓存结构:

win_data_cache

全局对象数据缓存结构与Node节点的十分类似,也是采用闭包中的一个局部变量作为缓存变量存储数据。但是对于普通对象的数据绑定便没有采用单独的缓存变量,原因是由于普通对象本身就具备一个良好的缓存特性,其实全局对象也是相同的,只是考虑到避免全局对象污染才采取了类似Node节点的数据缓存思想,使用具备变量做数据缓存。因此可以详见的是全局对象数据缓存实际是结合了普通对象数据缓存和Node节点数据缓存的方式,在缓存中使用每个KISSY库唯一的expando作为建立访问的桥梁。由于普通对象中可能还有其他很多expando,因此就需要这个唯一的expando标识其为数据缓存,这里全局对象数据也是使用同样的方法,虽然这个缓存对象不存在普通对象那个多个expando,但是依然统一地使用唯一expando开辟缓存空间,保证了代码的统一性。

既然全局对象数据缓存融合了普通对象数据缓存和Node节点数据缓存的特性,那么其实可以讲全局对象缓存实现在Node节点缓存中,由于Node节点缓存中是通过唯一的id来链接Node节点与数据缓存,那么我们同样可以给全局对象设置一个唯一id来表示,这样自然也就建立了唯一对应的数据缓存。只是KISSY的最终实现里面还是采用了普通对象数据缓存的结构,因此有了上图的结构。

实现

原理清楚了,那么现在大概了解一些KISSY.DOM.data(selector, name, data)函数的处理流程,首先根据selector选择元素,对于可能存在的多个元素,getter方法的原则是只返回第一个元素的值,而setter方法的原则则是设置每个元素的值。对于getter方法和setter方法是通过判断是否传入了data参数进行选择的。这里还需要注意的是,embed, object, applet这三个Node节点因为其特殊性,在其上设置数据可能会导致一定的风险,所以统一限制对于这三种标签的数据绑定。

在实现中还有一点儿需要注意的是每个KISSY库都有唯一的expando值,因为元素和缓存都是通过这个值进行链接的,如果页面中不小心再次引入了一个KISSY,相同的expando值会导致其中缓存的关系便会杂糅在一起,产生不可控异常。

总结一下,该函数的大致流程,可用如下图表示:

KISSY.DOM.data

最后还有一个removeData方法,这个方法的作用顾名思义就是移除用data方法增加了的数据。

- EOF -

KISSY深入研究(1)——kissy.js

本文着重从KISSY源代码研究,了解KISSY的整体组成,力求从原理上了解KISSY。笔者本人也是边看源代码边进行分析,做了一下笔记,以供大家参考交流,如果有任何不足和错误之处,希望大家提出来学习讨论!

如果你也clone过一份KISSY的源代码,那么从文件组织上面,src文件下的均是源代码文件,依照模块进行划分,这也是KISSY的代码组织的基本方式——模块。在src目录下,又有一个kissy文件夹,里面包含kissy.js,相比肯定是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的设计理念。

- EOF -

JavaScript的各种函数定义

今天正好有机会,和实习的一个童鞋讨论这个问题的时候,正好也对自己之前的困惑有了更好的解答,从MDC这篇文档(Functions and function scope)中搞清楚不少,记录下来,认真总结总结。

话说刚刚提到那片文档的内涵确实很丰富,不过今天着重要了解的是关于函数定义这块的。首先看一下JavaScript最常见的四种函数定义:

  1. 用Function构造函数定义的函数,代码如下:
    var multiply = new Function('x', 'y', 'return x * y;');
  2. 函数声明,这种方式也是最为常见的一种:
    function multiply(x, y) {
        return x * y;
    }
  3. 函数表达式,声明为匿名函数然后赋值给一变量,很常见的方式:
    var multiply = function(x, y) {
        return x * y;
    }
  4. 函数表达式,但是函数声明为命名函数再赋值给一变量,长得跟上一种方式真像:
    var multiply = function multi(x, y) {
        return x * y;
    }

首先比较一下函数名,以及将函数赋值给的那个函数变量直接的关系,真绕……直观一点儿,从刚刚的例4说吧,就是multiply这个函数变量与multi这个函数名的关系:

  • 函数名是不能够被修改的,相反的,函数变量是可以重新被赋值的。函数变量可以被重新赋值应该很好理解,我们第4个例子刚刚定义的multiply这个变量,看它不顺眼,重新赋值为:
    multiply = function(x, y) {
       return x + y;
    }

    立马摇身一变,从乘法变成加法了。但是multi这个函数变量想变就是不可能的了,函数定义已经在那儿了,只要还保留这它的引用,它就是不会变的,可能这里不大好理解,先这样想着,往下看,慢慢就应该能理解了。

  • 函数名同时是无法在函数外部使用的,它只在函数体内部可见,一个很简单的例子:
    var foo = function bar() {
        alert('hello');
    }
    foo(); // 提示“hello”字符串
    bar(); // 执行报错,bar未定义

    和明显,这里的bar确实是一个函数名,但是它确实不能在外部调用。这时候肯定会有童鞋问干嘛这个例子还是长得那么乖,和例4一个样,怎么不用例2的方式呢?问得好,且听我慢慢分解。

  • 继续说例4,我们可以看见函数名(multi)函数变量(multiply),本不相同,其实两者根本就没有任何关系,因此没有保持一致的必要。说到这儿,我想上面4个例子应该可以精简到3个,例2和例4本质应该是一致的。什么,不信?嘻嘻,我还得继续卖关子哈~继续读下去~~

我们发现例2和例4相比,只不过少了var的函数变量,而例3与例4相比,只不过少了那个函数名,这里从现象上看,例2和例4的本质是相同的,铁证如下:

function foo() {}
alert(foo); // 提示包含“foo”的函数名
var bar = foo;
alert(bar); // 提示依然只包含“foo”的函数名,和bar半毛钱关系都没有

的确是铁证吧?上面的类似例2的代码结合起来写是不是就成例4的方式了?正确,这就是我刚刚所说的两者本质应该相同,只是用例2方式定义函数的时候,JS引擎帮我们做了一些事情,比如声明了函数名为multiply的函数,同时还悄悄定义了一个也叫multiply的变量,然后赋值给这个变量,两个完全一样的名字,我们自以为在使用函数名multiply的时候,实际是在用multiply这个函数变量,晕了吧~说实话,我也晕了~~总之我们调用的时候,实在用函数变量调用,而函数名是无法在外部调用函数的,因此有了我上述的推断。

但是这里要提到的一个小小的差别,函数声明方式定义的函数,与构造函数声明的或者函数表达式声明的不同之处在于,函数声明方式的函数可以在函数定义之前就调用……不说了,还是看代码:

foo(); // 提示Foo
function foo() {
    alert('Foo');
}
bar(); // 哥们,和上面确实不一样,就不要逞能,这不报错了?提示bar未定义
var bar = function() {
    alert('Bar');
}

再说说构造函数声明的函数,这样声明的函数是不会继承当前声明位置的作用域,它默认只会拥有全局作用域,然而这个是其他几种函数声明方式也一样有的,如下:

function foo() {
    var hi = 'hello';
    //return function() {
    //    alert(hi);
    //};
    return Function('return hi;');
}
foo()(); // 执行效果大家自己跑一下看看

可以想见,用构造函数声明返回的这个函数执行必然报错,因为其作用域(即全局作用域)中没有hi这个变量。

还有一点,就是往往大家要说构造函数方式声明的函数效率要低,这是为什么呢?今天从文档是得知是因为另外3种方式申明的函数只会被解析一次,其实他们存在于闭包中,但是那也只与作用域链有关,函数体是只会被解析一次的。但是构造函数方式呢,每次执行函数的时候,其函数体都会被解析一次,我们可以想想这样声明的函数是一个对象,其中存放了参数以及函数体,每次执行的时候都要先解析一次,参数和函数体,才会执行,这样必然效率低下。具体实验不知道如何做?

最后说一个大家都不怎么注意的地方,什么时候看似函数声明方式的方式却不是函数生命方式(还是这么绕~简单点儿说,就是例2的方式什么时候在不经意间就成其他方式了):

  • 当成为表达式的一部分,就如同例3和例4。
  • 不再是脚本本身或者函数的“源元素”(source element)。什么是源元素呢?即在脚本中的非嵌套语句或者函数体(A "source element" is a non-nested statement in the script or a function body),例如:
    var x = 0;               // source element  
    if (x == 0) {            // source element  
       x = 10;               // not a source element, 因为嵌套在了if语句里
       function boo() {}     // not a source element, 因为嵌套在了if语句里
    }  
    function foo() {         // source element  
       var y = 20;           // source element  
       function bar() {}     // source element  
       while (y == 10) {     // source element  
          function blah() {} // not a source element, 因为嵌套在了while语句里
          y++;               // not a source element, 因为嵌套在了while语句里
       }  
    }  

    源元素的概念大概有了理解,继续刚刚说的函数声明,请看:

    // 函数声明
    function foo() {}  
      
    // 函数表达式
    (function bar() {})  
      
    // 函数表达式
    x = function hello() {}  
    
    if (x) {  
       // 函数表达式
       function world() {}  
    }
    
    // function statement  
    function a() {  
       // function statement  
       function b() {}  
       if (0) {  
          // 函数表达式
          function c() {}  
       }  
    }
    

最后这里说一下我自己的理解,之所以要区分函数声明与非函数声明,因为在我看了,函数声明方式的函数定义,在JS解析引擎执行的时候会将其提前声明,也就是像我们刚刚上面说的那样,可以在函数定义之前使用,实际上是解析引擎在我们使用前已经将其解析了,但是非函数声明式,就像表达式函数声明,JS解析引擎只会把var声明的变量提前定义,此时变量值为undefined,而真正对这个变量的赋值是在代码实际所在位置,因此上述提到报错都是undefined,实际变量已经定义了,只是还没有赋值,JS解析引擎不知道它为函数。

JavaScript中的函数概念真是博大精深啊,特别是再与对象挂起钩来,真是乱啊,援引圆心的一篇文章:与 Function 和 Object 相关的有趣代码 这其中的东西可真是太丰富了。

以上文字都是自己的理解+阅读文档得来,如果有什么不对,或者不恰当,或者不完整,或者……还请大家雅正,谢谢!

update: 2010-09-11 经过玉伯提醒,发现source element是mozilla自己的概念,其他JS引擎没有类似的规范,最后感谢玉伯给了一篇精彩的文章,Named function expressions demystified介绍我之前提到的这些,内容十分丰富,需要慢慢阅读。最后再次感叹一下,前端这潭水真的太深了~

tagName 与 nodeName

这两个概念,相信各位前端同学肯定都是比较清楚的,在JavaScript的开发中常常会用到,之前我一般都是用tagName,后来发现nodeName也能有一样的效果,但是他们直接的区别一直都不清楚,知道昨天看见了篇文章,讲得很清楚,因此就在这里翻译过来,也算加上自己的印象吧。

原文地址:http://aleembawany.com/2009/02/11/tagname-vs-nodename/

作者:Aleem Bawany

在JavaScript中检查HTML元素的名字,常常都要用到tagName和nodeName。通常情况下,两者都能达到同样的作用。如果你只支持A-grade浏览器的话,nodeName是一个更好的选择,但如果是你同样需要支持IE 5.5的话,那tagName却是更好的选择。

这里说一下tagName的两个问题:

  1. 在所有的IE浏览器中,一个注释节点(comment node)的tagName,总是会返回“!”。
  2. 对于文字节点(text node),tagName总是返回undefined,然而nodeName却返回“#text”。

但是nodeName也有自己的问题,但是影响不大:

  1. IE 5.5中,注释节点(comment node)的nodeName总是返回“!”,但它要比tagName好一些,因为nodeName只有在IE 5.5中,注释节点才会返回“!”,而其他版本IE均正常。
  2. IE 5.5中的文档元素(document element)以及属性节点(attribute node)的nodeName都失效。通常情况下,这些都不会造成什么问题,但是自己心里一定要有谱。
  3. Konqueror浏览器中使用nodeName的时候会自动忽略掉注释节点(comment node)。但是同样的,Konqueror和IE 5.5都不是A-grade浏览器

所以对于普通的JavaScript开发,还是应该坚持使用nodeName,因为它支持更广阔的应用场景,同时也有更好的向前兼容性。想想也知道,大家并不会因为它对于注释节点(comment node)的兼容性问题就放弃使用nodeName,相反,根本不用担心IE 5.5和Konqueror,因为他们的市场占用率都快趋近于0了。

Page 3 Of 2712345678...最旧 »