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"); } },