导言
Ext 封装了自己的事件机制,除了支持原始的鼠标和键盘事件之外,还能支持高级语义的事件,如 load 、 beforeLoad 、 render 、 destory 等等。
这一部分内容专门来解析 Ext 的事件机制,我打算用四种方法来解析这部分内容,力求稍有 js 基础的人都能看懂: 1 、源码注释翻译; 2 、执行流程图和源码片段截图; 3 、简单的 UML 类图; 4 、自定义测试代码。
这部分内容从最受大家关注的 Ext.onReady() 这个方法开始。 OK ,不要害怕,我会确保你能看懂的, Let’s go!
Ext.onReady()
大家经常会这么开始 Ext 的代码:
Ext.onReady(function(){
//……
})
这么做的目的是为了确保浏览器在文档加载完成之后再来执行我们的 js 。为了能更快更方便地理解 Ext.onReady() 这个方法到底做了什么,我们先来整理一下自己的思路。
书接上文,在“《仔仔细细分析 Ext 》 Ext 的事件模型之一 小沈阳的烦恼”一文中,我们自己制作了一段粗糙的代码来模拟这个过程,思路很简单:
第一步:先做一个简单的数组 var funcs=[] ,这个数组用来存放所有需要在文档一加载完成就开始执行的方法;
第二步:自己写一个叫 run() 的方法。这个方法只做一件事,它把第一步保存在 funcs 数据里面的函数全部拿出来依次执行一遍。
第三步:使用 js 原生的事件注册方式把第二步的 run() 方法注册到 window.onload 事件上。这样,一旦文档加载完成,这个方法就被执行。这样,我们预先放在 funcs 数组里面的方法就全部被执行了。
这段代码完整拿来看看:(原文请翻阅我的 blog:damoqiongqiu.iteye.com )
- <SPAN style="FONT-SIZE: medium">MyExt={ // 全局静态对象
- version:'1.0',
- isReady:false, // 文档是否加载完成标志
- funcs:[], // 缓存函数的数组
- onReady:function(f){
- if(MyExt.isReady) f();
- else MyExt.funcs.push(f);
- },
- run:function(){
- if(MyExt.isReady) return;
- for(var i=0;i<MyExt.funcs.length;i++){
- try{// 为了避免一个方法的错误影响到其它方法,一定要有 try 和 catch
- var f=MyExt.funcs[i];
- f();
- }catch(e){
- alert(" 事件发生异常了 "+e);
- }
- }
- MyExt.isReady=true;
- funcs=[];// 清空一下
- }
- };
- if(window.addEventListener){// 标准的 DOM2 事件注册
- window.addEventListener("load",MyExt.run,false);
- }else if(window.attachEvent){//IE 的事件注册
- window.attachEvent("onload",MyExt.run);
- }else {// 原始的 DOM0 的事件注册
- window.οnlοad=MyExt.run;
- }
- </SPAN>
MyExt={ // 全局静态对象
version:'1.0',
isReady:false, // 文档是否加载完成标志
funcs:[], // 缓存函数的数组
onReady:function(f){
if(MyExt.isReady) f();
else MyExt.funcs.push(f);
},
run:function(){
if(MyExt.isReady) return;
for(var i=0;i<MyExt.funcs.length;i++){
try{// 为了避免一个方法的错误影响到其它方法,一定要有 try 和 catch
var f=MyExt.funcs[i];
f();
}catch(e){
alert(" 事件发生异常了 "+e);
}
}
MyExt.isReady=true;
funcs=[];// 清空一下
}
};
if(window.addEventListener){// 标准的 DOM2 事件注册
window.addEventListener("load",MyExt.run,false);
}else if(window.attachEvent){//IE 的事件注册
window.attachEvent("onload",MyExt.run);
}else {// 原始的 DOM0 的事件注册
window.οnlοad=MyExt.run;
}
// 这样,我们就可以这么来写自己的代码,而不必关心文档加载的问题了。
MyExt.onReady(function(){
// 自己的代码
})
这段代码虽然简陋,但是核心的原理已经出来了:既然文档不加载完成我没办法干活,那好了,我不跟你纠缠,我先把要干的活都先存到一个筐子里面,等你(浏览器)觉得文档加载完了,可以干活了,你自己到这里来拿这些东西,一次性都给我干了吧!(貌似挺像批处理的撒!)
OK ,到这里应该没有什么难缠的地方吧?那么 OK ,恭喜你!因为不出意外的话,你肯定能很透彻地理解 Ext.onReady() 了!
Ext 采取的是类似的策略,只是实现的细节有所不同,下面我们来逐步深入地去解析。(下面的内容请不要跳着看。)
首先看一下 core 包下的 EventManage.js ,它里面有这么一句 :
很简单吧?原来 Ext.onReady 实际上执行的是 Ext.EventManager.onDocumentReady 这个方法。好,我们顺藤摸瓜,来看看 onDocumentReady 又是何方神圣。
还好,这段代码也不多。先不要管其它的东西,我们先来看这段代码本身的流程,然后再来看各个细节是怎么实现的:
如果 docReadyEvent 这个对象为空,就去执行 initDocReady() 这个方法;
接下来再判断,如果 docReadyState 或者 Ext.isReady 这两个标志位为真,立即执行传递进来的 function ,或者按照 options 这个参数里面的 delay 属性的值延迟执行传进来的 function 。
(备注: defer 方法是 Ext 对原生 Function 类的扩展,每个函数都有这个方法,作用是延迟一定的时间再执行。详细描述请翻阅我的 blog :“《仔仔细细分析 Ext 》
Ext 对 Function 类的扩展” )。
否则(文档还没初始化好),把我们传递进来的函数作为一个监听器,添加到 docReadyEvent 这个事件里面去。
结合我们自己做的那个小例子,到这个地方我们可以很敏感地推测出来,在添加监听器这个地方 Ext 肯定把我们传递进来的方法放到某个数组里面去了,然后等文档加载完成的时候再来调用这些方法,就像我们自己做的那样。
只是,这里的数组它不是一个简单的数组,而是作为 docReadyEvent 的一个属性出现的。来看看 docReadyEvent 到底是谁。沿着源码向上查找,我们发现了 docReadyEvent 的身影:
原来它是 Ext.util.Event 的一个实例,好,我们把 Ext.util.Event 揪出来(它是在 util 包下的 Observable.js 这个文件里面定义的):
哈哈,原来它是如此简单的一个对象!我们的推断没错,它里面果然用了一个数组 (listeners) 来放我们的 function !至于它的 addListener() 方法和最终怎么去调用我们传递进来的方法的(看它的 fire() 方法),这里不多罗嗦了,代码也很简单,原理就和我们自己制作的那个小例子相同。
好,到这里,感觉还没有什么太特别的地方。既然 Ext 在文档没有加载完的时候把我们的 function 存起来了,那么,在文档加载完之后它又是在哪里调用的呢?这个调用肯定与上面我们略过的 initDocReady() 这个方法有关,看代码:
- //检测文档是否加载完成
- var initDocReady = function(){
- docReadyEvent = new Ext.util.Event();
- if(Ext.isReady){
- return;
- }
- // no matter what, make sure it fires on load
- //确保文档加载完成事件能触发(这样fireDocReady()方法才能执行,从能设置文档状态并清除轮询定时器)
- E.on(window, 'load', fireDocReady);
- if(Ext.isGecko || Ext.isOpera) {//Gecko和Opera下的文档加载完成检测方法
- document.addEventListener('DOMContentLoaded', fireDocReady, false);
- }
- else if(Ext.isIE){//IE下的文档加载完成检测方法
- docReadyProcId = setInterval(function(){
- try{
- //这一句一直抛出异常,直到文档加载完成
- Ext.isReady || (document.documentElement.doScroll('left'));
- }catch(e){
- return;
- }
- fireDocReady(); //没有异常抛出,则说明文档已经加载完成,手工fire文档加载完成事件
- }, 5);
- document.onreadystatechange = function(){
- if(document.readyState == 'complete'){
- document.onreadystatechange = null;
- fireDocReady();
- }
- };
- }
- else if(Ext.isSafari){//Safari下的文档加载完成检测方法
- docReadyProcId = setInterval(function(){
- var rs = document.readyState;
- if(rs == 'complete') {
- fireDocReady();
- }
- }, 10);
- }
- };
//检测文档是否加载完成 var initDocReady = function(){ docReadyEvent = new Ext.util.Event(); if(Ext.isReady){ return; } // no matter what, make sure it fires on load //确保文档加载完成事件能触发(这样fireDocReady()方法才能执行,从能设置文档状态并清除轮询定时器) E.on(window, 'load', fireDocReady); if(Ext.isGecko || Ext.isOpera) {//Gecko和Opera下的文档加载完成检测方法 document.addEventListener('DOMContentLoaded', fireDocReady, false); } else if(Ext.isIE){//IE下的文档加载完成检测方法 docReadyProcId = setInterval(function(){ try{ //这一句一直抛出异常,直到文档加载完成 Ext.isReady || (document.documentElement.doScroll('left')); }catch(e){ return; } fireDocReady(); //没有异常抛出,则说明文档已经加载完成,手工fire文档加载完成事件 }, 5); document.onreadystatechange = function(){ if(document.readyState == 'complete'){ document.onreadystatechange = null; fireDocReady(); } }; } else if(Ext.isSafari){//Safari下的文档加载完成检测方法 docReadyProcId = setInterval(function(){ var rs = document.readyState; if(rs == 'complete') { fireDocReady(); } }, 10); } };
这段代码看似很长,实际上做的事情并不多。为了兼容浏览器,里面做了三个判断,我们来看最好懂的 IE 的情况,其它的都可以类比。
总体来看,它就两小段代码,第一段实际上是个定时器,每隔 5 毫秒就去尝试让 document 的滚动一下,如果能滚动(也就是 document 已经初始化完成了)就执行 fireDocReady 。从名称可以看出, fireDocReady 方法是在触发“文档准备好了”这个事件。可以推断,在这个方法里面一定会调用我们传进来的那些(被缓存)的方法。
看到第 119 行的 docReadyEvent.fire() 方法了吧,它先做了一些扫尾的工作,设置 Ext.isReady 标志位啦、清除定时器啦等等,然后就去调用了我们的 function 。
好了,解释到这个地方,其它的代码就无需多言了,应该都能轻松地看明白了。这里也提出一个小小的疑问:在 initDocReady() 这个方法里面,是用一个定时器来不断“重复错误”的方式来检测 document 是不是已经加载完成的。我曾经记得某本“经典”的 JAVA 或 C++ 书籍里面告诫程序员,不要企图使用“抛异常”的方式来做常规的、正常的流程判断。不知道有没有人研究过这块“尝试错误”的代码是不是存在什么样的问题(效率或内存)?如果你知道的话,请一定记得告诉我一声(群 88403922 )。