zoukankan      html  css  js  c++  java
  • 第十一章:事件系统

    事件系统是一个框架非常重要的部分,用于响应用户的各种行为。
    浏览器提供了3个层次的api,用于响应用户的各种行为


    1.最原始的是写在元素标签内。
    2.再次是脚本内,以el.onXXX = function绑定的方式,统称为DOM0事件系统。
    3.最后是多投事件系统,一个元素的同一类型事件可以绑定多个回调,统常称为DOM2事件系统。

    由于浏览器大战,现存两套API。

    IE与opera:
    绑定事件:el.attachEvent("on"+ type, callback)
    卸载事件:el.detachEvent("on"+ type. callback)
    创建事件:el.document.createEventObject()
    派发事件:el.fireEvent(type,event)

    w3c:

    绑定事件:el.addEventListener(type,callback,[phase])
    卸载事件:el.removeEventListener(type,callback,[phase])
    创建事件:el.createEvent(types)
    初始化事件:event.initEvent()
    派发事件:el.dispatchEvent(event)

    从api的数量与形式来看,w3c提供的复杂很多,相对于也强大很多,下面我们将逐一分析

    首先我们先来几个简单的例子,没必要动用框架。不过事实上,整个事件系统就建立在它们的基础上。

        function addEvent (el, callback, useCapture) {
            if(el.dispatchEvent){//w3c优先
                el.addEventListener(type, callback, !!useCapture );
            } else {
                el.attachEvent( "on"+ type, callback );
            }
            return callback; //返回callback方便卸载时用
        }
    
        function removeEvent (el, type, callback, useCapture) {
            if (el.dispatchEvent) { //w3c优先
                el.removeEventListener (type, callback, !!useCapture);
            } else {
                el.detachEvent( "on"+type, callback )
            }
        }
    
        function fireEvent (el, type, args, event) {
            args = args || {}
            if (el.dispatchEvent) { //w3c优先
                event = document.createEvent("HTMLEvents");
                event.initEvent(type, true, true);
            } else {
                event = document.createEventObject();
            }
    
            for (var i in args) if (args.hasOwnProperty(i)) {
                event[i] = args[i]
            }
    
            if (el.dispatchEvent) {
                el.dispatchEvent(event);
            } else {
                el.fireEvent('on'+type , event)
            }
        }

    一,onXXX绑定方式的缺陷

    onXXX既可以写在html标签内,也可以独立出来,作为元素节点的一个特殊属性来处理,不过作为一个古老的绑定方式,它很难预料到人们对这方面的扩展。

    总结下来有以下不足:

    1.onXXX对DOM3新增的事件或FF某些私有实现无法支持,主要有以下事件

    DOMActivate
    DOMAttrModified
    DOMAttributeNameChanged
    DOMCharacterDataModified
    DOMContentLoaded
    DOMElementNameChanged
    DOMFocusIn
    DOMFocusOut
    DOMMouseScroll
    DOMNodeInserted
    DOMNodeInsertedIntoDocument
    DOMNodeRemoved
    DOMNodeRemovedFromDcouemnt
    DOMSubtreeModified
    MozMousePixelScroll

    2.onXXX只允许元素每次绑定一个回调,重复绑定冲掉之前的绑定
    3.onXXX在IE下回调没有参数,在其他浏览器回调的第一个参数是事件对象
    4.onXXX只能在冒泡阶段可用

    二,attachEvent的缺陷

    attachEvent是微软在IE5添加的API,Opera也支持,也对于onXXX方式,它可以允许同一种元素同一种事件绑定多个回调,也就是所谓多投事件机制。但带来的麻烦只多不少,存在以下几点缺陷。

    1.ie下只支持微软的事件系统,DOM3事件一概不支持。
    2.IE下attchEvent回调中的this不是指向被绑定元素,而是window!
    3.IE下同种事件绑定多个回调时,回调并不是按照绑定时的顺序依次触发的!
    4.IE下event事件对象与w3c的存在太多差异了,有的无法对上号,比如currentTarget
    5.IE还是只支持冒泡阶段。


    关于事件对象,w3c是大势所趋,在IE9支持W3c那一套API时,这对我们实现事件代理非常有帮助。

    三,addEventListener的缺陷

    w3c这一套API也不是至善至美,毕竟标准总是滞后于现实,剩下的标准浏览器各有自己的算盘,它们之间也有不一致的地方。

    1.新事件非常不稳定,可能还有普及就开始被废弃,在早期的sizzle选择器引擎中,有这么几句。

        document.addEventListener("DOMAttrModified", invalidate, false);
        document.addEventListener("DOMNodeInserted", invalidate, false);
        document.addEventListener("DOMNodeRemoved", invalidate, false);

    现在这三个事件被废弃了(准确的说,所有变动事件都完蛋了),FF14和chrome18开始使用MutationObserver代替它。

    2.Firefox不支持focusin,focus事件,也不支持DOMFocusIn,DOMFocusOut,现在也不愿意用mouseWheel代替DOMMouseScroll。chrome不支持mouseenter与mouseleave.

    因此,不要以为标准浏览器就肯定实现了w3c标准事件,所有特征侦测必不可少。

    3.第三个,第四个,第五个标准参数。

    第三个参数,useCapture只有非常新的浏览器才是可选项。比如FF6或之前是可选的,为了安全起见,确保第三个参数为布尔

    4.事件成员的不稳定
    w3c是从浏览器商抄过来的,人家用了这么久,难免与标准不一致。

    ff下event.timeStamp返回0的问题,这个bug,2004年就有人提交了,直到2011年才被修复。

    Safari下event.target可能返回文本节点

    event.defaultPrevented,event.isTrusted与stopImmediatePropagation方法,之前标准浏览器都统一用getpreventDefault方法做这个事情,在jQuery源码中,发现它是用isDefaultPrevented来处理。

    isTrusted属性用于表示当前事件是否是由用户行为触发,比如是用一个真实的鼠标点击触发click事件,还是由一个脚本生成的(使用事件构造方法,比如event.initEvent)。isTrusted请多关注

    5.标准浏览器没有办法模拟像IE6-8的proprtychange事件

    虽然标准的浏览器有input, DOMAttrModified,MutationObserver,但比起propertychange弱爆了。propertychange可以监听多种属性变化,而不单单是value值。另外它不区分attribute和property。因此,无论是通过el.xxx = yyy 还是el.setAttribute(xxx,yyy)都接触此事件。

    http://www.cnblogs.com/rubylouvre/archive/2012/05/26/2519263.html (判断浏览器是否支持DOMAttrModified)

    四,Dean Edward的addEvent.js源码分析

    这是一个prototype时代早期出现的一个事件系统。jQuery事件系统源头。亮点如下:

    1.有意识的屏蔽IE与w3c在阻止默认行为与事件传播接口的差异。
    2.处理ie执行回调时的顺序问题
    3.处理ie的this指向问题
    4.没有平台检测代码,因为是使用最通用最原始的onXXX构建
    5.完全跨浏览器(IE4与NS4)。

    此处省略源码分析

    http://dean.edwards.name/weblog/2005/10/add-event/

    // written by Dean Edwards, 2005
    // with input from Tino Zijdel, Matthias Miller, Diego Perini
    
    // http://dean.edwards.name/weblog/2005/10/add-event/
    
    function addEvent(element, type, handler) {
        if (element.addEventListener) {
            element.addEventListener(type, handler, false);
        } else {
            // assign each event handler a unique ID
            if (!handler.$$guid) handler.$$guid = addEvent.guid++;
            // create a hash table of event types for the element
            if (!element.events) element.events = {};
            // create a hash table of event handlers for each element/event pair
            var handlers = element.events[type];
            if (!handlers) {
                handlers = element.events[type] = {};
                // store the existing event handler (if there is one)
                if (element["on" + type]) {
                    handlers[0] = element["on" + type];
                }
            }
            // store the event handler in the hash table
            handlers[handler.$$guid] = handler;
            // assign a global event handler to do all the work
            element["on" + type] = handleEvent;
        }
    };
    // a counter used to create unique IDs
    addEvent.guid = 1;
    
    function removeEvent(element, type, handler) {
        if (element.removeEventListener) {
            element.removeEventListener(type, handler, false);
        } else {
            // delete the event handler from the hash table
            if (element.events && element.events[type]) {
                delete element.events[type][handler.$$guid];
            }
        }
    };
    
    function handleEvent(event) {
        var returnValue = true;
        // grab the event object (IE uses a global event object)
        event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
        // get a reference to the hash table of event handlers
        var handlers = this.events[event.type];
        // execute each event handler
        for (var i in handlers) {
            this.$$handleEvent = handlers[i];
            if (this.$$handleEvent(event) === false) {
                returnValue = false;
            }
        }
        return returnValue;
    };
    
    function fixEvent(event) {
        // add W3C standard event methods
        event.preventDefault = fixEvent.preventDefault;
        event.stopPropagation = fixEvent.stopPropagation;
        return event;
    };
    fixEvent.preventDefault = function() {
        this.returnValue = false;
    };
    fixEvent.stopPropagation = function() {
        this.cancelBubble = true;
    };

    不过在Dean Edward对应的博文中就可以看到许多指正与有用的patch。比如说,既然所有的修正都是冲着IE去的,那么标准浏览器用addEventListener就行。有的还提到,在iframe中点击事件时,事件对象不对的问题,提交以下有用的补丁。

        event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);


    其中,第54条回复,直接导致了jQuery数据缓存系统的产生,为了避免交错引用产出的内存泄露,建议元素分配一个uuid,所有的回调都放在一个对象中存储

    但随着事件的推移,使用者发现onXXX在IE存在不可消除和弥补的内存泄露,因此,翻看jQuery早期的版本,1.01是照抄Dean Edward的,1.1.31版本,开始吸收54条uuid的建议,并使用attach/removeEventListener绑定事件——每个元素只绑定一次。然后所有回调都在类似handleEvent的函数中调用。

    五,jQuery1.8.2的事件模块概述

    jQuery的事件模块发端于Dean Edward的addEvent,然后它不断吸收社区的插件与补丁,发展成为非常棒的事件系统。其中不得不提的是事件代理与事件派发机制。


    早在07年,Brandon Aaron为jQuery写了一个划时代的插件,叫livequery,它可以监听后来插入的元素的事件。比如说,一个表格,我们为tr元素绑定了mouseover/mouseout事件时,只有十行代码,然后我们又动态加载了20行,这20个tr元素同样能执行mouseover/mouseout回调。魔术在于,它并没有把事件侦探器绑定在tr元素上,而是绑定在最顶层的document上,然后通过事件冒泡,取得事件源,判定它是否匹配给用户给定的css表达式,才执行用户回调、具体参考它的github:

    https://github.com/brandonaaron/livequery

    如果一个表格有100个 tr元素,每个都要绑定mouseover/mouseout事件,改成事件代理的方式,可以节省99次绑定,这优化很好,更何况它能监听将来添加的元素,因此被立马吸收到jquery1.3中去,成为它的live方法,再把一些明显的bug修复了。jquery1.32成为最受欢迎的版本。与后来的jquery1.42都是历程碑式的。 

    不过话说回来,live方法需要对某些不冒泡的事件做些处理,比如一些表单事件,有的只能冒泡的form,有的只能冒泡到document,有的根本就不冒泡

      ie6 ie8 ff3.6 opera10 chome4 sfari4
    submit form form document document document document
    reset form form document document form form
    change 不冒泡 不冒泡 document document 不冒泡 不冒泡
    click document document document document document document
    select 不冒泡 不冒泡 document document 不冒泡 不冒泡

    对于focus, blur, change, submit, reset, select等不会冒泡的事件,在标准浏览器中,我们我们可以设置addEventListener最后的一个参数为true轻松搞定在IE就有点麻烦了,要用focusin代替focus,focusout代替blur,selectsatrt代替select,change和submit,reset就更复杂了。必须使用其他的事件来模拟,还要判断事件源的类型,slelectedIndex, keyCode等相关属性

    这个课题到最后由一个叫reglib的库搞定,reglib的作者还写过一篇很著名的文章,《goodbuy mouseover,hello mouseenter》,来推广微软系统的两个事件,mouseenter与mouseleave。jQuery全面接纳了他们。

    live方法带来的全新体验是空前的,但毕竟要冒泡到最顶层,对IE来说有点坎坷,还会失灵。最好能指定父节点,一个绑定时已经存在的父节点。这样就不用费力了。当时有三篇博文给出了相近的方案,他们给出的接口一篇比一篇接近jhhon Resig接纳的方案。

    http://danwebb.net/2008/2/8/event-delegation-made-easy-in-jquery

    http://raxanpdi.com/blog-jquery-event-delegate.html (jQuery Event Delegation)

    http://blog.threedubmedia.com/2008/10/jquerydelegate.html(已经打不开)

    那时,它已经是这个样子

        $('div').delegate('click','span',function (event) {
            $(this).toggleClass('selected');
            return false;
        })

    并提出解除代理的API:undelegate

    在jquery1.42在2010年2月19推出时,也这两个接口,前面的两个参数只是掉换一下。

        $('div').delegate('span','click',function( event ){
            $( this ).toggleClass('selected');
            return false;
        })

    正所众人拾柴火焰高,jquery强大无不道理,在jquery1.8中,它又吸收dperini/nwevents的点子,改进其代理事件,大大 提高了性能。

    先看一下其主要接口:

    Jquery事件接口:bind live delegate on one trigger hover unbind die undelegate off toggle triggerHander Event .

    其中bind,unbind,one,trigger,toggle,hover,ready一开始就有。

    triggerHandler是jQuery1.23增加的,内部依赖于trigger,只对当前匹配元素的第一个有效,不冒泡不触发默认行为。

    live与die是jQuery1.3增加的,用于事件代理,统一由document代理

    delegate与undelegate是jquery1.4增加的,允许指定代理元素的事件代理,它内部是利用live,die实现的。

    on与off是jQuery1.7增加的,目的是统一事件接口,bind,one,live,delegate,直接由on衍生,unbind,die,dundelegate直接由off衍生。

    hover用于模拟css的hover效果,内部依赖于mouseenter,mouseleave

    ready可以看做是load事件的增强版,获取最早的DOM可用后,立即执行各种dom操作。

    toggle是click的增强版,每次点击都执行不同的回调,并切换到下一个。

    triggle与triggleHander是jQuery的fireEvent实现

    此外,jQuery还有25个事件类型命名的快捷方法,当参数个数为2时表现为绑定事件。个数为0时表现为派发事件

    jQuery快捷事件:contextmenu, error, keyup, keypress, keydown, submit, select, change, mouseleave, mouseenter, mouseout, mouseover, blur, focus, focusin, focusout, load, resize, scroll, unload, click, dbclik,mousedown, mouseup, mousemove

    不过,最重要最基础的设施无疑是jQuery.event之下

    add方法用于绑定事件。

    remove方法用于卸载事件。

    dispath方法用于统一用户回调,相当于Dean Edward的handleEvent方法,因此,jQuery1.7前它的名字一直叫handle。

    trigger用于事件派发。

    fix用于修正事件对象。

    自jQuery1.4起,jQuery大大强化自定义事件功能。special对象本来是用于修正个别事件的,现在它允许这些事件通过setup, teardown, add, remove, default这些接口实现DOM事件的各种行为

    参考:

    http://benalman.com/news/2010/03/jquery-special-events/

    六,jQuery.event.add源码解读

    add方法的主要目的,将用户所有的传递参数,合并成一个handleObj对象放到元素对应的缓存体中events对象的某个队列中,然后绑定一个回调。这个回调会处理用户的所有回调。因此,对于每个元素的每一种事件,它只绑定一次

        var add = function(elem, types, handler, data, selector) {
            var elemData, eventHandle, events, t, tns, namespaces, handleObj;
            //如果elem不能添加自定义属性,由于ie下访问文本节点会报错,因此事件源不能是文本节点。
            //注释节点本来就不应该绑定事件,注释节点之所以混进来,是因为jQuery的html方法所致
            //如果没有指定事件的类型或回调也立即返回,不再向下操作
            if (elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data(elem))) {
                return;
            }
            //取得用户回调与css表达式,handleObjIn这种结构我们称为事件描述
            //记下用户绑定此回调的各种信息,方便用于“事件拷贝”
            if (handler.handler) {
                handlerObjIn = handler;
                handler = handlerObjIn.handler;
                selector = handlerObjIn.selector;
            }
            //确保回调有uuid,用于查找和移除
            if (!handler.guid) {
                handler.guid = jQuery.guid++;
            }
    
            //为元素在数据缓存系统中开辟一个叫"event"的空间来保存其所有回调与事件处理器
            events = elemData.events;
            if (!events) {
                elemData.events = events = {};
            }
            eventsHandle = elemData.handle; //事件处理器
            if(eventHandle) {
                elemData.handle = eventHandle = function(e) {
                    //用户在事件冒充时,被第二次fire或者页面的unload后触发
                    return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? jQuery.event.dispatch.apply(eventsHandle.elem, arguments) : undefined;
                };
                //原注释是说,防止IE下非原生的事件内存泄露,(直接影响是明确了this的指向)
                eventHandle.elem  = elem;
            }
            //通过空格隔开同时绑定多个事件,比如jQuery(...).bind("mouseover mouseout", fn);
            types = jQuery.trim(hoverHack(types)).split(" ");
            for (t = 0 ; t < types.length; t++) {
                tns = rtypenamespace.exec(types[t]) || [] ; //取得命名空间
                type = tnsp[1];//取得真正的事件
                namespaces = (tns[2] || "").split(".").sort();//修正命名空间
                //并不是所有事件都能直接使用,比如FF下没有mousewheel,需要用DOMMouseScroll冒充
                special = jQuery.event.special[ type ] || {};
                //有时候,我们只需要在事件代理时进行冒充,比如FF下的focus, blur
                type = (selector ? special.delegateType : special.bindType) || type;
    
                special = jQuery.event.special[type] || {};
                //构建一个事件描述对象
                handleObj = jQuery.extend({
                    type : type,
                    origType : tns[1],
                    data : data,
                    handler : handler,
                    guid : handler.guid,
                    selector : selector,
                    needsContext : selector && jQuery.expr.match.needsContext.test(selector),
                    namespace : namespaces.join(".")
                },handlerObjIn);
    
                //在events对象上分门别类的存储事件描述,每种事件对于一个数组
                //每种事只绑定一次监听器(既addEventListener, attachEvent)
                handlers = events [type];
                if (!handlers) {
                    handlers = events[ type ] = [];
                    handlers.delegateCount = 0; //记录要处理的回调个数
                    //如果存在special.setup并且special.setup返回0个才直接使用多投事件API
                    if (!special.setup || special.setup.call(elem, data, namespaces, eventHandle) === false) {
                        if (elem.addEventListener) {
                            elem.addEventListener(type, eventHandle, false);
                        } else if (elem.attachEvent) {
                            elem.attachEvent("on" + type, eventHandle);
                        }
                    }
                }
                if (special.add) { //处理自定义事件
                    special.add.call(elem, handleObj);
                    if (!handleObj.handler.guid) {
                        handleObj.handler.guid = handler.guid;
                    }
                }
                //add to the element's hanlder list, deleate in front
                if (selector) { //如果是事件代理,那么把此事件代理描述放在数据的前面
                    handlers.splice(handlers.delegateCount++, 0, handleObj);
                } else {
                    handlers.push(handleObj);
                }
                //用于jQuery.event.trigger,如果此事件从未绑定过,也没必要进入trigger的真正处理逻辑
                jQuery.event.golbal[ type ] = true;
            }
            //防止ie内存泄露
            elem = null
        }

    从上面的注释中我们可以得出,jQuery的回调不再直接与元素挂钩,而是通过uuid访问数据缓存系统,抵达对于的events对象,再根据事件类型得到一组事件描述。并且事件描述里边没有事件源的记录,因此,非常方便导出挪动,为事件克隆大开方便之门。当然,其中数据缓存系统是关键,接着探索,就会发现,其它事件代理部分对数据缓存依赖的更严重。

    下面是元素数缓存与事件描述之间的结构

    在firebug下查看结构:如下

     

    七,jQuery.event.remove的源码解读

    remove方法的主要目的是,根据用户传参,找到事件队列,从里边把匹配的handleObj对象移除,在参数不足的情况,可能移除N个或所有。当队列的长度为零就移除事件,当events为空对象,则清除掉UUID

        remove = function(elem, types, handler, selector) {
            var t, tns, type, origType,namespaces, origCount, j, events, special, eventType, handleObj,
            elemData = jQuery.hasData( elem ) && jQuery._data( elem );
            //如果不支持添加自定义属性或没有缓存与事件有关的东西,立即返回。
            if ( !elemData || !(events = elemData.events)) {
                return;
            }
            //hover转换为"mouseenter mouseleave",并且按照空格进行切割,方便移除多种事件类型
            types = jQuery.trim( hoverHack( types || "")).split(" ");
            for (t = 0; t < types.length; t++) {
                tns = rtypenamespace.exec(types[t]) || [];
                type = origType = tns[1]; //取得事件类型
                namespaces = tns[2];//取得命名空间
                if (!type) { //如果没有指定事件类型,则移除所有事件类型或移除所有与此命名空间相关的事件类型
                    for (type in events) {
                        jQuery.event.remove( elem, type + types[t], handler, selector, true);
                    }
                    continue;
                }
                //利用事件冒充,取得真正的用于绑定事件类型
                special = jQuery.event.special[ type ] || {};
                type = (selector ? special.delegateType : special.bindType) || type;
                eventType = events[type] || []; //取得装载事件描绘对象的数组
                origCount = eventType.length;
                //取得用于过滤命名空间的正则,没有为null
                namespaces = namespaces ? new RegExp("(^|\.)" + namespaces.split(".").sort().join("\.(?:.*\.|)") + "(\.|$)" : null ;
    
                    //移除符合条件的事件描述
                    for ( j = 0; j < eventType.length; j++) {
                        handleObj = eventType[ j];
                        if (( origType === handleObj.guid ) & //比较事件类型是否一致
                            (!handler || handler.guid === handleObj.guid) && //如果传递了回调,判定UUid是否相同
                            //如果types含义命名空间,用正则看是否匹配
                            //如果事件代理必有css表达式,比较与事件描述对象中是否相等
                            (!selector || selector === handleObj.selector || selector === "**" && handleObj.selector)) {
                            eventType.splice(j-- , 1); //是就移除
                            if ( handleObj.selector ) { //同时delegateCount减去1
                                eventType.delegateCount--;
                            } 
    
                            if (special.remove) { //处理个别事件移除
                                special.remove.call( elem, handleObj);
                            }
                        }
                    }
                    //如果已经移除所有此类问题,则卸载框架绑定去的elemData.handle
                    //origCount !== eventType.length 是为了防止死循环
                    if ( eventType.length === 0 && origCount !== eventType.length) {
                        if (!special.teardown || special.teardown.call(elem, namespaces, elemData.handle ) === false) {
                            jQuery.removeEvent (elem, type, elemData.handle); 
                        }
                        delete events[ type ];
                    }
            }
            //如果events为空,则从elemData中删除events与handler
            if ( jQuery.isEmptyObject( events )) {
                delete elemData.handle;
                jQuery.removeData (elem, "events", true)
            }
        }


    事件卸载部分是jQuery事件系统中最简单的部分,主要逻辑都花在移除事件描述对象和匹配条件上。

    八,jQuery.event.dispatch的源码解读

    这是jQuery事件系统的核心。它就是利用这个dispatch方法,从缓存体中的events对象取得对于的队列,然后修复事件对象,逐个传入用户的回调中执行,根据返回值决定是否断开循环(stopImmediatePropagation),阻止默认行为和事件传播。

        dispatch = function(event) {
            //创建一个伪事件对象(jQuery.Event实列),从真正的实际对象上抽得响应的属性附于其上
            //如果是iE,也可以将其转换成对于的W3c属性,抹去两大平台的差异。
            event = jQuery.event.fix(event || window.event);
    
            var i, j, cur, ret, selMatch, matched, matches, handleObj, sel, related,
            //取得所有事件描述对象
                 handlers = ((jQuery._data(this, "events") || {}) [ event.type ] || []),
                delegateCount = handlers.delegateCount,
                args = core_slice.call(arguments),
                run_all = !event.exclusive && !event.namespace,
                special = jQuery.event.special[ event.type ] || {},
                handlerQueue = [];
            //重置第一个参数为jQuery.Event实例
            args[0] = event;
            event.delegateTarget = this; //添加一个人为的属性,用于事件代理
            //执行preDispatch回调,它与后面的postDispatch构成一种类似AOP的机制
            if (special.preDispatch && special.preDispatch.call(this, event) === false) {
                return;
            }
    
            //如果是事件代理,并且不是来自非左键的点击事件
            if (delegateCount && !(event.button && event.type === "click")) {
                //从事件源开始,遍历其所有祖先一直到绑定事件的元素
                for ( cur = event.target; cur != this; cur = cur.parentNode || this) {
                    //不要触发被disabled元素的点击事件
                    if (cur.disabled !== tru event.type !== "click") {
                        selMatch = {}; //为了节能起见,每种CSS表达式只判定一次,通过下面的
                        //jQuery( sel, this).index( cur ) >= 0 或 jQuery.find(sel, this, null, [ cur ].length)
                        matches = []; //用于收集符合条件的事件描述对象
                        //使用事件代理的事件描述对象总是排在最前面
                        for (i = 0; i < delegateCount; i++ ) {
                            handleObj = handler[ i ];
                            sel = handleObj.selector;
    
                            if (selMatch[ sel ] === undefined) {
                                //有多少个元素匹配就收集多少个事件描述对象
                                selMatch[ sel] = handleObj.needsContext ? jQuery(sel, this).index(cur) >= 0 : jQuery.find(sel, this, null, [cur]).length;
                            }
                            if (selMatch[ sel ]){
                                matches.push(handleObj);
                            }
                        }
                        if (matches.length) {
                            handlerQueue.push({elem: cur, matches : matches});
                        }
                    }
                }
            }
            //取得其他直接绑定的事件描述对象
            if (handlers.length > delegateCount) {
                handlerQueue.push({elem : this, matches : handlers.slice(delegateCount) })
            }
    
            //注意:这个循环是从下到上执行的
            for (i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++) {
                matched = handlerQueue[ i ];
                event.currentTarget = matched.elem;
                //执行此元素的所有与event.type同类型的回调,除非用户调用了stopImmediatePropagation方法,它会导致isImmediatePropagetionStopped返回true,从而中断循环
                for (j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++) {
                    handleObj = matched.matches[j];
                    //最后的过滤条件为事件命名空间,比如著名的bootstrap的命名空间为data-api
                    if (run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test(handleObj.namespace)) {
                        event.data = handleObj.data;
                        event.handleObj = handleObj;
                        //执行用户回调,(有时 可能还要外包一层,来自jQuery.event.specal[type].handle)
                        ret = ((jQuery.event.special[ handleObj.origType ] || {}).handle || 
                            handleObj.handler).apply(matched.elem, args);
                        //根据结果判定是否阻止事件传播与默认行为
                        if (ret !== undefined) {
                            event.resulet = ret;
                            if (ret === false) {
                                //
                                event.preventDefault();
                                event.stopPropagation();
                            }
                        }
                    }
                }
            }
            //执行postDispatch回调
            if (special.postDispatch) {
                special.postDispatch.call(this, event);
            }
            return event.resulet;
        }

    本节的难点在于如何模拟事件的传播机制,jQuery实际上只模拟冒泡那一阶段。

    九,jQuery.event.trigger解读

    笼统的来说,triggter就是dispatch的加强版。

    dispatch只触发当前元素与其底下元素(通过事件代理的方式)的回调,trigger则模拟整个冒泡过程,除了它自身,还触发其祖先节点与window的同类型的回调。不过,从trigger的代码来看,它比dispatch多做的事情就是触发事件的默认行为

    这涉及到太多的判定,如果再把dispatch的代码写在一块就不好维护了。

    但是,我觉得,trigger其实用不着那么复杂,trigger要做的事情就是在某一元素触发一个回调(dispatch),生产一个事件对象,然后让其顺序冒泡,触发其它的回调(dispatch)就行了。

    浏览器提供了原生的派发机制,IE下的fireEvent,标准浏览器为dispatchEvent.

    如果在控制台执行以下函数,我们会得到更多的事件构造器

        Object.getOwnPropertyNames(window).filter(function (p) {
            return typeof window[p] == "function" && (window[p].prototype instanceof Event)
        })
    ["MIDIMessageEvent", "MIDIConnectionEvent", "MediaKeyMessageEvent", "MediaEncryptedEvent", "webkitSpeechRecognitionEvent", "webkitSpeechRecognitionError", "StorageEvent", "SpeechSynthesisEvent", "MediaStreamEvent", "IDBVersionChangeEvent", "GamepadEvent", "DeviceOrientationEvent", "DeviceMotionEvent", "CloseEvent", "OfflineAudioCompletionEvent", "AudioProcessingEvent", "MediaKeyEvent", "XMLHttpRequestProgressEvent", "WheelEvent", "WebGLContextEvent", "UIEvent", "TransitionEvent", "TrackEvent", "TouchEvent", "TextEvent", "SecurityPolicyViolationEvent", "SVGZoomEvent", "ProgressEvent", "PopStateEvent", "PageTransitionEvent", "MutationEvent", "MouseEvent", "MessageEvent", "MediaQueryListEvent", "KeyboardEvent", "HashChangeEvent", "FocusEvent", "ErrorEvent", "CustomEvent", "CompositionEvent", "ClipboardEvent", "BeforeUnloadEvent", "AutocompleteErrorEvent", "ApplicationCacheErrorEvent", "AnimationEvent", "WebKitAnimationEvent", "WebKitTransitionEvent"]

    (webkit包含)

    但常用的交互都集中在HTML4.0已经定义好的事件上,我们无需理会message storage popstate事件,更别提事件变动。我们认为支持以下事件就好了

    HTMLEvents Load, unload, abort, error, select, change, submit, reset, focus, blur, resize, scroll
    KeyboardEvent keypress keydown keyup
    MouseEvents contextmenu, click, dblclick, mouseout, mouseover, mouseenter, mouseleave, mousemove,mousedown, mouseup,mousewheel

    根据上面的表格,我们做一个叫eventMap的hash出来,那么trigger方法最大限制级可以压缩成如下样子

        trigger: function (type, target) {
            var doc = target.ownerDocument || target.document || target || document ;
            event = doc.createEvent(eventMap[type] || "CustomEvent");
            if (/^(focus|blur|select|submit|reset)$/.test(type)) {
                target[type] && target[type] (); //触发默认行为
            }
            Event.initEvent( type, true, true);
            target.dispatchEvent(event);
        }


    但是这样trigger出来的对象是只读,不能覆盖原生属性或方法,因此,你可以以为它自定义一个more属性,里边装载着你要改的东西,然后在dispatch将它包装成一个jQuery伪事件对象后,在把循环加在伪事件对象就行 了。

    十,jQuery对事件对象的修复(不更新)

    十一,滚轮事件的修复(不更新)

    十二,mouseenter与mouseleave事件的修复(不更新)

    十三,focus与focusout事件的修复(不更新)

    十四,旧版本下IE的submit的事件代理实现(不更新)

    在旧版本的IE下submit不会冒泡到顶层,它只执行form元素的submit回调,并立即执行提交跳转,因此只能用事件冒充的方式来实现

    我们看看什么情况下浏览器触发submit事件吧。submit事件与鼠标事件,键盘事件是不一样的。它是一种复合事件,既可以使用鼠标实现,也可以通过键盘事件实现,重要的结果,能实现表单提交即可。

    当焦点聚焦于input[type=text]、input[type=password]、input[type=checkbox]、input[type=radio]、input[type=button]、input[type=image]、input[type=submit]、按回车键就会触发提交

    当鼠标在input[type=image],input[type=submit]上方,通过点击事件就会触发提交
    浏览器差异,IE下可以在input=file回车后触发
    我们也可以使用form.submit()这样的编程手段触发

    IE下submit, reset的事件代理,delegate用于检测它有没有用事件代理,不用管它。重点在于它在代理元素上一下绑定了两个事件,click,keypress,如果是键盘事件,就判断他的keycode是否为13108(回车键)

    el.submit()方法有个特异之处,它是不会执行submit回调的,像其他click,blur,focus,select这样的DOM方法都会同时执行回调与默认行为

    十五,oninput事件的兼容性性处理(不更新)

    本章已更新完毕

    上一章:第十章:属性模块 下一章:第十二章:异步处理

  • 相关阅读:
    defer与async的区别
    Promise 的含义
    SCSS 与 Sass 异同
    CSS总结2
    CSS总结1
    jQuery-插件,优化
    jQuery-表格以及表单
    jQuery-事件以及动画
    jQuery-ajax
    jQuery-DOM操作
  • 原文地址:https://www.cnblogs.com/ahthw/p/4697139.html
Copyright © 2011-2022 走看看