支付宝赞助帐号:service@kuitao8.com 

jQuery.event 事件机制 全解析(一)

Created2014-03-05   Views 29624    Author 懒人程序
参考源码 jQuery 1.9.1 , 2013/4/19 jQuery 2.0 推出。

jQuery 的事件机制由以下几部分组成:

1、利用 jQuery.Event(event | type[, props]) 构造函数创建可读写的 jQuery事件对象 $event, 该对象即可以是原生事件对象 event 的增强版,也可以是用户自定义事件,第二个参数 props 可以是一个对象, $event 会复制这个对象的属性来增强自己;

2、依赖 jQuery.event.fix(event),将原生的事件对象 event 修正为一个 $event 对象,并对该 $event 的属性以及方法统一接口。该方法在内部调用了 jQuery.Event(event) 构造函数。

3、由核心组件 jQuery.cache 实现注册事件处理程序的存储,实际上绑定在 DOM元素上的事件处理程序只有一个,即 jQuery.cache[elem[expando]].handle 中存储的函数,该函数在内部调用 jQuery.event.dispatch(event) 实现对该DOM元素特定事件的缓存的访问,并依次执行这些事件处理程序。

4、jQuery.event.dispatch(event) 方法会依次调用该DOM元素(this)上绑定的特定事件类型(event.type)的所有事件处理程序,并通过检测 handleObj.selector 与其后代元素(这里指从 event.target 到 this 路径上的所有DOM节点)的匹配情况,来处理基于选择器过滤的事件委托机制。jQuery.event.dispatch(event) 方法在进入逻辑时就调用  jQuery.event.fix(event) 方法对 event 对象进行修正。

5、jQuery.event.dispatch(event) 方法在处理事件委托机制时,依赖委托节点在DOM树的深度安排优先级,委托的DOM节点层次越深,其执行优先级越高。而其对于stopPropagation的处理有些特殊,在事件委托情况下并不一定会调用绑定在该DOM元素上的该类型的所有事件处理程序,而依赖于委托的事件处理程序的执行结果,如果低层委托的事件处理程序声明了停止冒泡,那么高层委托的事件以及自身绑定事件就不会被执行,这拓展了 DOM 委托机制的功能。

6、jQuery.event.trigger(event | type, data, elem, onlyHandlers) 方法提供开发人员以程序方式触发特定事件的接口,该方法的第一个参数可以是 $event/ event 对象 ,也可以是某个事件类型的字符串 type; 第二个参数 data 用于扩展该事件触发时事件处理程序的参数规模,用于传递一些必要的信息。 elem参数表示触发该事件的DOM元素;最后该方法在默认情况下,其事件会冒泡,并且在有默认动作的情况下执行默认行为,但是如果指定了 onlyHandlers 参数,该方法只会触发绑定在该DOM元素上的事件处理程序,而不会引发冒泡和默认动作,也不会触发特殊的 trigger 行为。

7、namespace 命名空间机制,namespace 机制可以对事件进行更为精细的控制,开发人员可以指定特定空间的事件,删除特定命名空间的事件,以及触发特定命名空间的事件。这使得对事件处理机制的功能更加健壮。

8、jQuery.event.add(elem, types, handler, data, selector) 方法用于给特定elem元素添加特定的事件 types([type.namespace, type.namespace, ...])的事件处理程序 handler, 通过第四个参数 data 增强执行当前 handler 事件处理程序时的 $event.data 属性,以提供更灵活的数据通讯,而第五个元素用于指定基于选择器的委托事件。

9、与之相反的,jQuery.event.remove(elem, types, handler, selector, mappedTypes) 方法用于删除该elem元素中满足特定选择器 selector 和特定事件类型 types 的事件处理程序 handler。 第五个参数 mappedTypes 只在该方法内部调用时使用,用于移除所有事件(不限事件类型,但可能有命名空间)时该参数才为 true,表示不需要检测类型是否匹配就可以删除事件处理程序(主要是用于处理 mouseenter 这类高阶事件,因为其存储在 mouseover 之中)。

10、高阶事件origType 与原生事件type: 我们将浏览器原生支持的事件称为原生事件,比如 mouseover; 而将基于原生事件进行增强的事件称为高阶事件 origType,比如 mouseenter,这里的 orig 字样表示这是用户输入的原始事件名。一般来说,高阶事件依赖于特定的原生事件才能实现,比如 mouseenter 来说,其 jQuery.event.specail.mouseenter.deletype = mouseover ,这说明 mouseenter 高阶事件依赖于 mouseover 原生事件来实现,相对来说, mouseenter 的事件处理程序需要某些执行条件。

11、模拟事件并立刻触发 jQuery.event.simulate(type, elem, event, bubble) 方法,可用于在DOM元素 elem 上模拟自定义事件类型 type,参数 bubble用于指定该事件是否可冒泡,event 参数表示 jQuery 事件对象 $event。 模拟事件通过事件对象的isSimulated属性为 true 表示这是模拟事件。该方法内部调用 trigger() 逻辑 或 dispatch() 逻辑立刻触发该模拟事件。该方法主要用于修正浏览器事件的兼容性问题,比如模拟出可冒泡的 focusin/ focusout 事件,修正IE中 change 事件的不可冒泡问题,修正IE中 submit事件不可冒泡问题。 (在jQuery 2.0 中只需要修正 focusin/ focusout 事件)

12、jQuert.event.special 对象用于某些事件类型的特殊行为和属性。比如 load 事件拥有特殊的 noBubble 属性,可以防止该事件的冒泡而引发一些错误。总的来说,有这样一些方法和属性:

       setup()  ——  若指定该方法,其在绑定事件处理程序(addEventListener)前执行,如果返回 false, 才绑定事件处理程序;

       teardown() —— 若指定该方法,其在移除事件处理程序(removeEventListener)前执行,如果返回 false,才移除事件处理程序;

       trigger() —— 若指定该方法,在执行 $.event.trigger 核心逻辑前调用该方法,如果返回 false,跳出 $.event.trigger 函数;

       handle(event) —— 特殊的事件处理程序,若指定该方法,会替代其原来的事件处理程序 handleObj.handler 而被调用。

       preDispatch(elem, event) —— 若指定该方法,在执行 $.event.dispatch 核心逻辑前调用该方法,如果返回 false,跳出 $.event.dispatch 方法。

       postDispatch(elem, event) —— 若指定该方法,在执行 $.event.dispatch 核心逻辑后调用该函数,进行额外调度。

       noBubble —— 指定该事件类型不能进行冒泡。

       deleType, bindType —— 指定该事件类型的低阶事件类型,deleType 表示委托, bindType 表示非委托。

 

jQuery 1.9.1 版本实际上公开的API包括, on、 off、 trigger、triggerHandler 四个接口。

bind、unbind、delegate、undelegate、one 方法仅仅是 on/off API的简单外观模式,在编写jQuery代码时可以避免。

on/one 函数的核心逻辑依赖于 jQuery.event.add()

off 函数依赖于 jQuery.event.remove()

trigger/ triggerHandler 依赖于 jQuery.event.trigger(),主要用于通过程序触发事件,而不是依赖于用户交互,这些事件可以是自定义事件,也可以是DOM规范的事件, jQuery.event.trigger() 还可以进行冒泡和执行特定事件的默认行为。

jQuery.event.dispatch() 方法用于调度地执行特定的事件处理程序,除了DOM规范的事件之外,还包括模拟基础事件而产生的 focusin/ focusout(可冒泡),以及便利的 mouseenter/ mouseout(只会在当前元素触发该事件,不冒泡),以及自定义事件 。dispatch是事件系统实现的内部机制,并不对外暴露。

1、jQuery.Event 构造函数

jQuery.Event 是一个构造函数,其创建一个可读写的jQuery事件对象(我们约定,后面称之为 $event,将浏览器原生事件对象称为 event,以示区分)。由原生事件对象生成的 $event 对象保留了对这个原生事件对象 event 的引用($event.originalEvent)。我们绑定的事件处理程序所处理的事件对象都是 $event。

该方法也可以传递一个自定义事件的类型名,用于生成用户自定义事件对象。

jQuery.Event = function(src, props) {        
    if (!(this instanceof jQuery.Event)) {
        return new jQuery.Event(src, props);
    }

    // src 是原生事件对象 event
    if (src && src.type) {
        this.originalEvent = src;        // $event.originalEvent 保留对原生事件对象 event 的引用
        this.type = src.type;             
        // $event 的 isDefaultPrevented 属性应与 原生事件对象 event 保持一致
        this.isDefaultPrevented = (src.defaultPrevented || src.returnValue === false ||
            src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse;
    // src 是 type 时
    } else {
        this.type = src;
    }
 
    // 扩展 $event 对象
    if (props) {
        jQuery.extend(this, props);
    }

    this.timeStamp = src && src.timeStamp || jQuery.now();        // 时间戳属性,这是 DOM3 Event 新增属性
    this[jQuery.expando] = true;                           // 表示该事件对象是 $event, jQuery事件对象
};

jQuery.Event.prototype = {
    isDefaultPrevented: returnFalse,                  // 默认情况下,这三个属性都是 false
    isPropagationStopped: returnFalse,
    isImmediatePropagationStopped: returnFalse,
    preventDefault: function() {                      // 同时修改 $event 和 event
        var e = this.originalEvent;
        this.isDefaultPrevented = returnTrue;
        e && e.preventDefault();
    },
    stopPropagation: function() {
        var e = this.originalEvent;
        this.isPropagationStopped = returnTrue;
        e && e.stopPropagation();
    },
    stopImmediatePropagation: function() {
        this.isImmediatePropagationStopped = returnTrue;
        this.stopPropagation();
    }
};

 

2、$Event 对象的跨浏览器兼容性

jQuery 利用 jQuery.event.fix() 来解决跨浏览器的兼容性问题,统一接口。除该核心方法外,统一接口还依赖于 (jQuery.event) props、 fixHooks、keyHooks、mouseHooks 等数据模块。

props 存储了原生事件对象 event 的通用属性,keyHook.props 存储键盘事件的特有属性,而mouseHooks.props 存储鼠标事件的特有属性。

keyHooks.filter 和 mouseHooks.filter 两个方法分别用于修改键盘和鼠标事件的属性兼容性问题,用于统一接口。比如 event.which 通过 event.charCode 或 event.keyCode 或 event.button 来标准化。

最后 fixHooks 对象用于缓存不同事件所属的事件类别,比如

fixHooks['click'] === jQuery.event.mouseHooks;

fixHooks['keydown'] === jQuery.event.keyHooks;

fixHooks['focusin'] === {};

这个过程只在第一次处理时进行,再次处理时可以依赖缓存实现性能优化。

复制代码
fix: function(event) {
        if (event[jQuery.expando]) {                // 若该对象已是 $event ,不需要 fix 过程。
            return event;
        }

        // 依赖原生 event 对象创建一个可读写的 $event 对象并且标准化该对象的属性
        var i, prop, copy,
            type = event.type,
            originalEvent = event,
            fixHook = this.fixHooks[type];                // 该事件类型的 fixHook

        if (!fixHook) {    // 初始化处理  /^(?:mouse|contextmenu)|click/   /^key/
            fixHook = this.fixHooks[type] = rmouseEvent.test(type) ? this.mouseHooks :
                rkeyEvent.test(type) ? this.keyHooks : {};
        }
        
        // 由原生event创建的$event对象,目前只有 type、 originalEvent、timestamp 等属性。
        event = new jQuery.Event(originalEvent);

        // copy 存储了当前事件类型的所有属性列表,由通用属性 + 特有属性组合而成
        copy = fixHook.props ? this.props.concat(fixHook.props) : this.props;

        // 将原生对象的对应属性复制到 $event 对象中
        i = copy.length;
        while (i--) {
            prop = copy[i];
            event[prop] = originalEvent[prop];
        }
        
        // 处理通用属性的浏览器兼容性问题
        // ....

        // 处理特定类型属性的浏览器兼容性问题
        fixHook.filter && fixHook.filter(event, originalEvent)
        
        return event;        //返回 $event
    },    

3、缓存 与 handleObj

我们添加的事件处理函数并不直接绑定在DOM元素上,事实上,jQuery将我们定义的事件处理程序都存储在该DOM元素对应的缓存 jQuery._data(elem) 上的events 属性中,events 对象按照不同的事件类型存放不同的事件处理程序,比如下图中的 click是其中的一种事件类型。

这里的 click (events[type])是一个数组结构,数组的每个元素是一个 handleObj 对象,其存储了 jQuery事件机制实现所必要的属性,比如命名空间机制 namespace; 附加数据机制 data(附加到 $event.data 中);事件委托机制 selector、needsContext; handleObj 与 handler 关联机制 guid ;  高阶事件实现机制 origType、type 等。实际我们所定义的事件处理函数将会存储在 handleObj.handler 属性中,该函数通过全局唯一的guid属性实现与 handleObj 的通信。

下面观察 jQuery.event.add() 和 jQuery.event.remove() 方法的源码:

 jQuery.event.add() 的核心逻辑: 

      1、 初始化 elem 元素的事件缓存 (events、handle、events[type]);

      2、 给事件处理程序 handler 一个全局唯一标识 guid(如果已有,就不需要重新赋值),该标识也将赋值给对应的 handleObj,作为通信渠道;

      3、 创建一个handleObj,该 handleObj 拥有 selector、guid、handler、data、needsContext、type、origType、namespace 属性;

      4、 将该 handleObj 添加到 events[type] 队列中,委托事件在前,直接绑定事件在后,保持有序。

add: function(elem, types, handler, data, selector) {
        var events, len, handleObjIn,
            special, eventHandle, handleObj,
            handlers, namespaces,
            
            elemData = jQuery._data(elem);

        if (!elemData) {     // 如果该 elem 不能拥有数据 或是 text/comment 节点
            return;
        }

        // 调用该 add() 方法时可以传递一个定制对象来代替 handler 事件处理函数,我觉得用处不大
        if (handler.handler) {
            handleObjIn = handler;               
            handler =   handleObjIn.handler;
            selector =  handleObjIn.selector;
        }

        // 确保该事件处理程序拥有一个唯一标识, 我们通过这个标识实现查询和移除remove操作,通过该属性也可以实现移除多个相同函数。
        if (!handler.guid) {
            handler.guid = jQuery.guid++;
        }

        // 如果这是第一次给该元素添加事件,初始化处理 events 和 handle 属性
        events = elemData.events;
        if (!events) {
            events = elemData.events = {};
        }
        eventHandle = elemData.handle;
        if (!eventHandle) {
            eventHandle = elemData.handle = function(e) {
                // 定义真*事件处理程序, 如果在页面 unload 后触发该事件; 在执行默认行为时(trigger)时避免事件的二次冒泡执行
                if (typeof jQuery !== 'undefined' && (!(e && jQuery.event.triggered === e.type))) {
                    return jQuery.event.dispatch.apply(eventHandle.elem, arguments);     //  通过dispatch进行调度分发
                } else {
                    return;
                }
            };    
            // Add elem as a property of the handle to prevent a memory leak with IE non-native events
            eventHandle.elem = elem;
        }

        // Handle multiple events separated by space   jQuery(..).bind("mouseover mouseout", fn);
        types = (types || "").match(core_rnotwhite) || [""];
        len = types.length;
        while (len--) {
            var tmp = rtypenamespace.exec(types[len]) || [];      //  必定返回一个数组
            var type = origType = tmp[1];                    //  原生类型 和 高阶类型
            var namespaces = (tmp[2] || "").split(".").sort();    //  命名空间

            special = jQuery.event.special[type] || {};
            type = (selector ? special.delegateType : special.bindType) || type;  // 修正原生事件类型
            special = jQuery.event.special[type] || {};                           // 该原生事件类型的特殊对象,这里主要是 add 和 setup

            // 处理 handleObj 对象,添加其必要属性
            handleObj = jQuery.extend({
                type: type,
                origType: origType,
                data: data,
                handler: handler,
                guid: handler.guid,
                selector: selector,
                needsContext: selector && jQuery.expr.match.needsContext.test(selector),
                namespace: namespaces.join(".")                    //经排序的命名空间,已经修正
            }, handleObjIn);

            // 如果这是第一次添加该类型事件, 初始化该类型事件的队列
            handlers = events[type];      
            if (!handlers) {
                handlers = events[type] = [];
                handlers.delegateCount = 0;        // 初始化委托事件的计数为 0

                //满足以下条件: 没定义special.setup 或是 调用该函数的结果是 false, 使用 addEventListener
                if (special.setup === undefined || special.setup.call(elem, data, namespaces, eventHandle) === false) {
                    elem.addEventListener(type, eventHandle, false);      
                }
            }

            // 如果有自己的特殊方法 add,调用该方法。
            if (special.add) {
                special.add.call(elem, handleObj);
                if (!handleObj.handler.guid) {
                    handleObj.handler.guid = handler.guid;
                }
            }
            
            // 委托事件放在前面,插入handleObj对象到元素特定事件类型的列表中。这是核心逻辑,保持有序和优先级
            if (selector) {
                handlers.splice(handlers.delegateCount, 0, handleObj);
                handlers.delegateCount++;
            } else {
                handlers.push(handleObj);
            }
            
            // 记录状态, 用于事件优化。  好像没有什么作用....
            jQuery.event.global[type] = true;
        }

        // 去除对 elem 的引用, 防止IE中的内存溢出
        elem = null;
    },

jQuery.event.remove() 的核心逻辑是:

       1、删除特定的事件处理程序,支持命名空间匹配、选择器匹配、handler匹配、类型匹配;

       2、删除事件处理程序后,维护 jQuery.cache 的数据结构,及时清空或移除数据,及时解除事件的绑定;

复制代码
remove: function(elem, types, handler, selector, mappedTypes) {
        var j, handleObj, tmp,
            origCount, t, events,
            special, handlers, type,
            namespaces, origType,
            elemData = jQuery.hasData(elem) && jQuery._data(elem);       //确保该DOM元素有数据的情况下才获取数据

        if (!(elemData && (events = elemData.events))) {
            return;
        }

        // Once for each type.namespace in types;  type 可省略,只有 .namespace 比如 types[i] 为'.hello'
        types = (types || "").match(core_rnotwhite) || [""];
        t = types.length;
        while (t--) {
            tmp = rtypenamespace.exec(types[t]) || [];
            type = origType = tmp[1];
            namespaces = (tmp[2] || "").split(".").sort();

            // 移除所有类型的事件,可以带 namespaces,也可以不带。设定参数mappedTypes为true 这样才能删除高阶事件origType,比如 mouseenter
            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;  //获取原生事件,高阶事件存储在原生事件中

            handlers = events[type] || [];
            tmp = tmp[2] && new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)");

            // 移除匹配的事件,origCount 表示队列的原始长度,后面用于对比是否有真删除事件。
            origCount = j = handlers.length;
            while (j--) {
                handleObj = handlers[j];

                // 如果 mappedTypes 或 用户输入类型与该事件处理对象的高阶类型相同
                // 并且 没有指定事件处理程序(即任意) 或 该事件处理程序 guid 与 该事件处理对象 guid 匹配
                // 并且 没有指定命名空间(对命名空间无需求) 或 命名空间匹配
                // 并且 没有指定选择器(非委托) 或 选择器匹配 或 (选择器为 ** ,而且该处理对象有选择器)
                // '**'  ---> 表示匹配所有有选择器的事件处理函数,即任何委托的事件处理函数
                if ((mappedTypes || origType === handleObj.origType) &&
                    (!handler || handler.guid === handleObj.guid) &&
                    (!tmp || tmp.test(handleObj.namespace)) &&
                    (!selector || selector === handleObj.selector || 
                         (selector === "**" && handleObj.selector))) {
                    
                    // 核心逻辑
                    handlers.splice(j, 1);

                    if (handleObj.selector) {
                        handlers.delegateCount--;
                    }
                    
            // 特殊处理机制
                    if (special.remove) {    
                        special.remove.call(elem, handleObj);
                    }
                }
            }

            // 后期维护:移除该类型的事件处理函数,并清除其缓存,如果已经不需要。避免在移除特殊事件的处理器时发生潜在的无限递归
            if (origCount > 0 && handlers.length === 0) {
                if (!special.teardown || special.teardown.call(elem, namespaces, elemData.handle) === false) {
                    jQuery.removeEvent(elem, type, elemData.handle);
                }
                delete events[type];
            }
        }

        // 后期维护,及时处理元素的缓存
        if (jQuery.isEmptyObject(events)) { 
               delete elemData.handle; 
               jQuery._removeData(elem, "events"); 
        } 
},
复制代码

上一篇: jQuery 核心 - noConflict() 方法
下一篇: jQuery.Callbacks() 回调函数队列 分析
支持键盘 ← →

邮件订阅

订阅我们的精彩内容