ExtJs源码分析与学习—ExtJs事件机制(一)

      前面讲了ExtJs核心代码以及扩展后,今天来说说ExtJs的事件机制,要想弄明白ExtJs的事件机制,就必须先知道浏览器的事件机制,这里给出了浏览器事件机制与自定义事件的实现 。  


     首先看源码 ext-base-event.js 关于浏览器本身事件的封装。代码中实现了各主要浏览器的兼容,以及对一些事件进行了扩展。该代码中首先定义了类Ext.lib.Event,该类(函数)是一个匿名函数自执行,执行后返回对象pub,pub赋值给Ext.lib.Event。

 

Ext.lib.Event = function() {
    var loadComplete = false,
        unloadListeners = {},//用来存放el的unload事件
    …
    var pub = {
  …
};
    return pub;
}();

 既然该类是围绕pub来实现的,我们首先来看pub的定义,pub中定义了许多关于事件处理的方法

 

onAvailable : function(p_id, p_fn, p_obj, p_override) {
            onAvailStack.push({
                id:         p_id,
                fn:         p_fn,
                obj:        p_obj,
                override:   p_override,
                checkReady: false });

            retryCount = POLL_RETRYS;
            startInterval();
        },

 该方法中调用了startInterval()和_tryPreloadAttach()。_tryPreloadAttach() 、 onAvailable() 、startInterval() 这三个函数的执行机制大致是这样的:在文档还没有加载完成之前,可以通过 onAvailable() 方法给某个对象注册某类事件的监听器, onAvailable() 方法会调用 startInterval() 方法来启动一个轮询来执行 _tryPreloadAttach() ,轮询的周期是 POLL_INTERVAL (默认是 20ms ),轮询次数是 POLL_RETRYS ( 200 次)(这么来看的话,这种依靠不断轮询来尝试文档是否已经加载完成的方法最长是 20ms*200=4 秒钟)。 _tryPreloadAttach() 方法里面会判断文档是不是已经加载完成,如果加载完成,执行注册的监听器,并把定时器清除掉。_tryPreloadAttach() 方法还有一个 tryAgain 标志用来说明是不是要进行再次尝试。

 

addListener: function(el, eventName, fn) {
            el = Ext.getDom(el);
            if (el && fn) {
                if (eventName == UNLOAD) {
                    if (unloadListeners[el.id] === undefined) {
                        unloadListeners[el.id] = [];
                    }
                    unloadListeners[el.id].push([eventName, fn]);
                    return fn;
                }
                return doAdd(el, eventName, fn, false);
            }
            return false;
        },

 该方法为元素添加注册事件,el为添加事件的元素,eventName为事件名称(如click),fn为响应函数(hanlder)。对“unload”事件做了单独处理,内部调用了私有的doAdd函数。

 

doAdd = function() {
            var ret;
            if (win.addEventListener) {//标准浏览器
                ret = function(el, eventName, fn, capture) {
                    if (eventName == 'mouseenter') {
                        fn = fn.createInterceptor(checkRelatedTarget);
                        el.addEventListener(MOUSEOVER, fn, (capture));
                    } else if (eventName == 'mouseleave') {
                        fn = fn.createInterceptor(checkRelatedTarget);
                        el.addEventListener(MOUSEOUT, fn, (capture));
                    } else {
                        el.addEventListener(eventName, fn, (capture));
                    }
                    return fn;
                };
            } else if (win.attachEvent) {//ie浏览器,ie9中会同时支持这两种方式win.attachEvent和win.addEventListener,
                ret = function(el, eventName, fn, capture) {
                    el.attachEvent("on" + eventName, fn);
                    return fn;
                };
            } else {
                ret = function(){};
            }
            return ret;
        }(),

 

该函数为闭包函数,即自执行函数,会返回ret对应的函数。并且添加了其他浏览器(IE除外)不支持的事件mouseenter和mouseleave ,为非IE浏览器间接实现了这两个事件,需要另两个函数的辅助。这两个辅助函数的实现也可以用到其他没有引入ExtJs的项目中。

 

function checkRelatedTarget(e) {
        return !elContains(e.currentTarget, pub.getRelatedTarget(e));
}
//判断某个元素child是否是parent的子元素,是则返回true,否则false。
    function elContains(parent, child) {
       if(parent && parent.firstChild){
         while(child) {
            if(child === parent) {
                return true;
            }
            child = child.parentNode;
            //nodeType 属性返回被选节点的节点类型。等于1为节点Element,当child不是parent的孩子节点时,会一直执行,直到child.nodeType 为 9 document时
            if(child && (child.nodeType != 1)) {
                child = null;
            }
          }
        }
        return false;
}

 

elContains 两个参数parent,child判断某个元素child是否是parent的子元素,是则返回true,否则false。
checkRelatedTarget 会作为一个拦截器,这里e.currentTarget是指添加事件的元素本身。pub.getRelatedTarget(e)返回的是跟该事件相关的元素标准浏览器用relatedTarget IE中用fromElement,toElement。在Ext.lib.Dom中也有个实现判断父子元素的方法isAncestor,后续会讲到。不知道ExtJs为什么要写两个实现,个人推测这两段代码可能是两个人实现的,并且实现的原理有些不同,故保留了两个。

 

下面看getRelatedTarget的实现

 

getRelatedTarget : function(ev) {
            ev = ev.browserEvent || ev;
            return this.resolveTextNode(ev.relatedTarget ||
                (/(mouseout|mouseleave)/.test(ev.type) ? ev.toElement :
                 /(mouseover|mouseenter)/.test(ev.type) ? ev.fromElement : null));
        },

 

 

这里有几个浏览器事件对象属性需说明一下

target 指事件源对象,点击嵌套元素最里层的某元素,该元素就是target。IE6/7/8对应的是srcElement。

currentTarget 指添加事件handler的元素本身,如el.addEventListener中el就是currentTarget。IE6/7/8没有对应属性,可在handler内使用this来替代如evt.currentTarget = this。

relativeTarget 指事件相关的元素,一般用在mouseover,mouseout事件中。IE6/7/8中对应的是fromElement,toElement。

 

getRelatedTarget 方法返回相关的元素结点,在该方法中resolveTextNode辅助函数又判断了火狐和其他浏览器的不同。

 

resolveTextNode : Ext.isGecko ? function(node){
            if(!node){
                return;
            }
            // work around firefox bug, https://bugzilla.mozilla.org/show_bug.cgi?id=101197
            var s = HTMLElement.prototype.toString.call(node);
            if(s == '[xpconnect wrapped native prototype]' || s == '[object XULElement]'){
                return;
            }
            return node.nodeType == 3 ? node.parentNode : node;
        } : function(node){
            return node && node.nodeType == 3 ? node.parentNode : node;
        },

 

下面看与addListener对应的方法removeListener

 

       /**
         * 删除 el 事件
         */
        removeListener: function(el, eventName, fn) {
            el = Ext.getDom(el);
            var i, len, li, lis;
            if (el && fn) {
                if(eventName == UNLOAD){
                    if((lis = unloadListeners[el.id]) !== undefined){
                        for(i = 0, len = lis.length; i < len; i++){
                            if((li = lis[i]) && li[TYPE] == eventName && li[FN] == fn){
                                unloadListeners[el.id].splice(i, 1);
                            }
                        }
                    }
                    return;
                }
                doRemove(el, eventName, fn, false);
            }
        },

 

该方法与addListener对应,用来删除由addListener注册的事件,实际调用是利用Ext.EventManager中的方法来调用,该类对这两个方法又进行了更进一步的封装,后续待分析。该方法中用到了辅助函数doRemove

 

       /**
         * 返回一个符合当前浏览器的函数用来注销事件,
            对于非ie下的浏览器也让其可以支持mouseleave/mouseenter
         */
        doRemove = function(){
            var ret;
            if (win.removeEventListener) {
                ret = function (el, eventName, fn, capture) {
                    if (eventName == 'mouseenter') {
                        eventName = MOUSEOVER;
                    } else if (eventName == 'mouseleave') {
                        eventName = MOUSEOUT;
                    }
                    el.removeEventListener(eventName, fn, (capture));
                };
            } else if (win.detachEvent) {
                ret = function (el, eventName, fn) {
                    el.detachEvent("on" + eventName, fn);
                };
            } else {
                ret = function(){};
            }
            return ret;
        }();
 

 

除了注册和删除事件,Ext还对其他原生的事件方法(属性)进行了封装,看下面

 

getTarget : function(ev) {
            ev = ev.browserEvent || ev;
            return this.resolveTextNode(ev.target || ev.srcElement);
        },

 获取当前事件源对象

 

getPageX : function(ev) {
            return getPageCoord(ev, "X");
        },

        getPageY : function(ev) {
            return getPageCoord(ev, "Y");
        },


        getXY : function(ev) {
            return [this.getPageX(ev), this.getPageY(ev)];
        },

 这三个函数用来获取鼠标事件源的坐标(水平,垂直)坐标,其中调用了辅助函数getPageCoord和getScroll,该方法中解决了不同浏览器之间的不同,实现了兼容。

 

function getPageCoord (ev, xy) {
        ev = ev.browserEvent || ev;
        var coord  = ev['page' + xy];
        if (!coord && coord !== 0) {
            coord = ev['client' + xy] || 0;

            if (Ext.isIE) {
                coord += getScroll()[xy == "X" ? 0 : 1];
            }
        }

        return coord;
}

私有的getPageCoord方法用来获取鼠标事件时相对于文档的坐标(水平,垂直)。

Firefox引入了pageX   /  Y   ,IE9/Safari/Chrome/Opera虽然支持但仅在文档(document)内而非页面(page)。

Safari/Chrome/Opera可以使用标准的clientX/Y获取,IE下可通过clientX/Y与scrollLeft/scrollTop计算得到。

 

 

 

function getScroll() {
        var dd = doc.documentElement,
            db = doc.body;
        if(dd && (dd[SCROLLTOP] || dd[SCROLLLEFT])){
            return [dd[SCROLLLEFT], dd[SCROLLTOP]];
        }else if(db){
            return [db[SCROLLLEFT], db[SCROLLTOP]];
        }else{
            return [0, 0];
        }
    }

 私有的getScroll方法返回文档的scrollTop和scrollLeft值,由于浏览器差异,该实现上先从document.documentElement取,为0后再从document.body上取。都没有的话返回[0,0]。

 

 

下面看另外三个方法

 

       stopEvent : function(ev) {
            this.stopPropagation(ev);
            this.preventDefault(ev);
        },

        stopPropagation : function(ev) {
            ev = ev.browserEvent || ev;
            if (ev.stopPropagation) {
                ev.stopPropagation();
            } else {
                ev.cancelBubble = true;
            }
        },

        preventDefault : function(ev) {
            ev = ev.browserEvent || ev;
            if (ev.preventDefault) {
                ev.preventDefault();
            } else {
                ev.returnValue = false;
            }
        },

 preventDefault 为阻止事件的默认行为。W3C标准使用 preventDefault 方法,IE6/7/8则是设置 returnValue 为false。Safari/Chrome/Opera同时支持IE6/7/8方式。Firefox仅支持标准的preventDefault。IE9两者都支持。


stopPropagation 用来停止事件冒泡,阻止事件进一步向下传播。关于事件传播的三个阶段:捕捉阶段、到达目标对象阶段、起泡阶段的详细描述可参阅《 JavaScript 权威指南》

stopEvent 则同时阻止默认行为和事件冒泡

 

下面几个方法只把源码贴出来,功能可以看注释。注意的是getListeners和purgeElement在Ext.EventManager中会实现,等讲到哪里再详细的分析

 

         /**
         * 获取事件对象
         */
        getEvent : function(e) {
            e = e || win.event;
            if (!e) {
                var c = this.getEvent.caller;
                while (c) {
                    e = c.arguments[0];
                    if (e && Event == e.constructor) {
                        break;
                    }
                    c = c.caller;
                }
            }
            return e;
        },

        /**
         * 获取按键码,注意在keypress 事件中使用。
         */
        getCharCode : function(ev) {
            ev = ev.browserEvent || ev;
            return ev.charCode || ev.keyCode || 0;
        },

        //clearCache: function() {},
        // deprecated, call from EventManager
        /**
         * 获取注册在某个事件类型上的所有监听器
         */
        getListeners : function(el, eventName) {
            Ext.EventManager.getListeners(el, eventName);
        },

        // deprecated, call from EventManager
        /**
         * 清除注册在某个事件类型上的所有监听器
         */
        purgeElement : function(el, recurse, eventName) {
            Ext.EventManager.purgeElement(el, recurse, eventName);
        },

        /**
         * 如果文档已经加载完成,如果是IE,则将注册在 window 的 onload 事件上的监听器全部清除掉
         */
        _load : function(e) {
            loadComplete = true;
            
            if (Ext.isIE && e !== true) {
                // IE8 complains that _load is null or not an object
                // so lets remove self via arguments.callee
                doRemove(win, "load", arguments.callee);
            }
        },

        /**
         * 在文档卸载之前,把注册在 window 的 unload 事件上的所有监听函数执行一遍,然后从缓存数组中清除掉。
         */
        _unload : function(e) {
             var EU = Ext.lib.Event,
                i, v, ul, id, len, scope;

            for (id in unloadListeners) {
                ul = unloadListeners[id];
                for (i = 0, len = ul.length; i < len; i++) {
                    v = ul[i];
                    if (v) {
                        try{
                            scope = v[ADJ_SCOPE] ? (v[ADJ_SCOPE] === true ? v[OBJ] : v[ADJ_SCOPE]) :  win;
                            v[FN].call(scope, EU.getEvent(e), v[OBJ]);
                        }catch(ex){}
                    }
                }
            };

            Ext.EventManager._unload();

            doRemove(win, UNLOAD, EU._unload);
        }
 

另外在Ext.lib.Event的开头定义了一些变量

 

var loadComplete = false,
        unloadListeners = {},//用来存放el的unload事件
        retryCount = 0,
        onAvailStack = [],
        _interval,
        locked = false,
        win = window,
        doc = document,

        // constants
        POLL_RETRYS = 200,
        POLL_INTERVAL = 20,
        TYPE = 0,
        FN = 1,
        OBJ = 2,
        ADJ_SCOPE = 3,
        SCROLLLEFT = 'scrollLeft',
        SCROLLTOP = 'scrollTop',
        UNLOAD = 'unload',
        MOUSEOVER = 'mouseover',
        MOUSEOUT = 'mouseout',

 将window、document等外部(非函数作用域内)变量存于本地变量中,这样JS压缩程序就能对它们进行名称替换,获得更高的压缩率。

 

以上代码为ExtJs对浏览器原生事件的简单封装,而对浏览器本身的事件进行转换和调用,是通过类Ext.EventObject和Ext.EventManager实现的,请看后续讲解。

 

你可能感兴趣的:(extjs)