第十一章:事件系统

优质
小牛编辑
135浏览
2023-12-01

事件系统是一个框架非常重要的部分,用于响应用户的各种行为。
浏览器提供了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,有的根本就不冒泡。

 ie6ie8ff3.6opera10chome4sfari4
submitformformdocumentdocumentdocumentdocument
resetformformdocumentdocumentformform
change不冒泡不冒泡documentdocument不冒泡不冒泡
clickdocumentdocumentdocumentdocumentdocumentdocument
select不冒泡不冒泡documentdocument不冒泡不冒泡

对于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事件,更别提事件变动。我们认为支持以下事件就好了

HTMLEventsLoad, unload, abort, error, select, change, submit, reset, focus, blur, resize, scroll
KeyboardEventkeypress keydown keyup
MouseEventscontextmenu, 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事件的兼容性性处理(不更新)

 

(本章已更新完毕)

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