欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

jQuery源码分析(13)-事件绑定(2)

程序员文章站 2022-05-06 10:08:57
...

事件绑定与执行的具体流程:

一、事件预绑定

1、jQuery.fn.on主要通过jQuery.event.add函数达到添加事件处理程序的目的。源码解析:

//给选中的元素注册事件处理程序
add: function(elem, types, handler, data, selector) {

	var handleObjIn, eventHandle, tmp,
		events, t, handleObj,
		special, handlers, type, namespaces, origType,
		/*
		jQuery从1.2.3版本引入数据缓存系统,贯穿内部,为整个体系服务,事件体系也引入了这个缓存机制。
		所以jQuery并没有将事件处理函数直接绑定到DOM元素上,而是通过.data存储在缓存.cahce上。
		*/
		//获取数据缓存(在$.cahce缓存中获取存储的事件句柄对象,如果没就新建elemData)
		elemData = data_priv.get(elem);

	// Don't attach events to noData or text/comment nodes (but allow plain objects)
	//检测状态,若为空数据、text或comment节点时,阻止绑定事件
	if (!elemData) {
		return;
	}

	// Caller can pass in an object of custom data in lieu of the handler
	// 一般在第一运行的时候,handler为事件处理函数,后面jQuery对handler做了一些包装
	// 检测handler是包含handler和selector的对象,包含说明handler是一个事件处理函数包
	if (handler.handler) {
		handleObjIn = handler;
		handler = handleObjIn.handler;
		selector = handleObjIn.selector;
	}

	// Make sure that the handler has a unique ID, used to find/remove it later
	//检测handler是否存在ID (guid),如果没有那么传给他一个ID
	//添加ID的目的是 用来寻找或者删除handler(因为handler是缓存在缓存对象上的,没有直接跟元素节点发生关联)
	if (!handler.guid) {
		handler.guid = jQuery.guid++;
	}
	
	// Init the element's event structure and main handler, if this is the first
	// elemData:缓存的元素数据
	/*
	在elemData中有两个重要的属性,
		一个是events,是jQuery内部维护的事件列队
		一个是handle,是实际绑定到elem中的事件处理函数
		之后的代码无非就是对这2个对象的筛选,分组,填充了
	*/
	// events:事件处理程序 列队{type:[事件处理对象,事件处理对象]}
	// 检测缓存数据中没有events数据
	if (!(events = elemData.events)) {
		events = elemData.events = {};
	}

	// 如果缓存数据中没有handle数据 (此时handle为定义事件处理器)
	if (!(eventHandle = elemData.handle)) {
		//eventHandle并没有直接处理回调函数,而是映射到jQuery.event.dispatch分派事件处理函数了
		eventHandle = elemData.handle = function(e) {
			// Discard the second event of a jQuery.event.trigger() and
			// when an event is called after a page has unloaded
			// typeof jQuery !== core_strundefined 检测jQuery是否被注销
			// 取消jQuery.event.trigger第二次触发事件
			// jQuery.event.dispatch 执行处理函数
			return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ?
				jQuery.event.dispatch.apply(eventHandle.elem, arguments) :
				//没有传递回调对象,是因为回调的句柄被关联到了elemData,也就是内部数据缓存中了
				undefined;
		};
		// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
		// 定义事件处理器对应的元素,用于防止IE非原生事件中的内存泄露
		//这个元素没有直接让事件直接引用了,而是挂在数据缓存句柄上,很好的避免了这个IE泄露的问题
		eventHandle.elem = elem;
	}

	// Handle multiple events separated by a space
	// jQuery(...).bind("mouseover mouseout", fn);
	// 事件可能是通过空格键分隔的字符串,所以将其变成字符串数组
	// core_rnotwhite:/\S+/g
	types = (types || "").match(core_rnotwhite) || [""];
	// 例如:'.a .b .c'.match(/\S+/g) → [".a", ".b", ".c"]
	// 事件的个数
	t = types.length;

	while (t--) {
		// 尝试取出事件的命名空间
		// 如"mouseover.a.b" → ["mouseover.a.b", "mouseover", "a.b"]
		tmp = rtypenamespace.exec(types[t]) || [];
		// 取出事件类型,如mouseover
		type = origType = tmp[1];
		// 取出事件命名空间,如a.b,并根据"."分隔成数组
		namespaces = (tmp[2] || "").split(".").sort();

		// There *must* be a type, no attaching namespace-only handlers
		if (!type) {
			continue;
		}

		// If event changes its type, use the special event handlers for the changed type
		// 事件是否会改变当前状态,如果会则使用特殊事件
		special = jQuery.event.special[type] || {};

		// If selector defined, determine special event api type, otherwise given type
		// 根据是否已定义selector,决定使用哪个特殊事件api,如果没有非特殊事件,则用type
		type = (selector ? special.delegateType : special.bindType) || type;

		// Update special based on newly reset type
		// type状态发生改变,重新定义特殊事件
		special = jQuery.event.special[type] || {};

		// handleObj is passed to all event handlers
		// 这里把handleObj叫做事件处理对象,扩展一些来着handleObjIn的属性
		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);

		// Init the event handler queue if we're the first
		// 初始化事件处理列队,如果是第一次使用,将执行语句
		/*
		事件委托从队列头部推入,而普通事件绑定从尾部推入,
		通过记录delegateCount来划分,委托(delegate)绑定和普通绑定。
		*/
		if (!(handlers = events[type])) {
			handlers = events[type] = [];
			handlers.delegateCount = 0;

			// Only use addEventListener if the special events handler returns false
			// 如果获取特殊事件监听方法失败,则使用addEventListener进行添加事件
			/*
			elem:目标元素;
			type:事件类型,如'click';
			eventHandle:事件句柄,也就是事件回调处理的内容。(由上面eventHandle的创建,可知eventHandle
				不仅仅只是充当一个回调函数的角色,而是实现了EventListener接口的对象。)
			false:冒泡;(表示在冒泡阶段执行事件,如果是true,则在捕获阶段执行事件)
			*/
			if (!special.setup || special.setup.call(elem, data, namespaces, eventHandle) === false) {
				if (elem.addEventListener) {
					elem.addEventListener(type, eventHandle, false);
				}
			}
		}

		// 特殊事件使用add处理
		if (special.add) {
			special.add.call(elem, handleObj);
			// 设置事件处理函数的ID
			if (!handleObj.handler.guid) {
				handleObj.handler.guid = handler.guid;
			}
		}

		// Add to the element's handler list, delegates in front
		// 将事件处理对象推入处理列表,姑且定义为事件处理对象包
		if (selector) {
			handlers.splice(handlers.delegateCount++, 0, handleObj);//冒泡标记
		} else {
			handlers.push(handleObj);
		}

		// Keep track of which events have ever been used, for event optimization
		// 表示事件曾经使用过,用于事件优化
		jQuery.event.global[type] = true;
	}

	// Nullify elem to prevent memory leaks in IE
	// 设置为null避免IE中循环引用导致的内存泄露
	elem = null;
},

events,eventHandle 都是elemData缓存对象内部的,可见在elemData中有两个重要的属性,

一个是events,是jQuery内部维护的事件列队;

一个是handle,是实际绑定到elem中的事件处理函数。

add函数的目的就是把事件处理程序填充到events,eventHandle中。

涉及:

多事件处理

如果是多事件分组的情况jQuery(...).bind("mouseover mouseout", fn);

事件可能是通过空格键分隔的字符串,所以将其变成字符串数组

增加命名空间处理

事件名称可以添加指定的event namespaces(命名空间) 来简化删除或触发事件。例如,"click.myPlugin.simple"为 click 事件同时定义了两个命名空间 myPlugin 和 simple。通过上述方法绑定的 click 事件处理,可以用.off("click.myPlugin") 或 .off("click.simple")删除绑定到相应元素的Click事件处理程序,而不会干扰其他绑定在该元素上的“click(点击)” 事件。命名空间类似CSS类,因为它们是不分层次的;只需要有一个名字相匹配即可。以下划线开头的名字空间是供 jQuery 使用的。

引入jQuery的Special Event机制

什么时候要用到自定义函数?有些浏览器并不兼容某类型的事件,如IE6~8不支持hashchange事件,你无法通过jQuery(window).bind('hashchange', callback)来绑定这个事件,这个时候你就可以通过jQuery自定义事件接口来模拟这个事件,做到跨浏览器兼容。

原理

jQuery(elem).bind(type, callbakc)实际上是映射到 jQuery.event.add(elem, types, handler, data)这个方法,每一个类型的事件会初始化一次事件处理器,而传入的回调函数会以数组的方式缓存起来,当事件触发的时候处理器将依次执行这个数组。

jQuery.event.add方法在第一次初始化处理器的时候会检查是否为自定义事件,如果存在则将会把控制权限交给自定义事件的事件初始化函数,同样事件卸载的jQuery.event.remove方法在删除处理器前也会检查此。例如:

if (!special.teardown || special.teardown.call(elem, namespaces, elemData.handle) === false) {
				jQuery.removeEvent(elem, type, elemData.handle);
			}

jQuery.event.special对象中,保存着为适配特定事件所需的变量和方法,

具体有:
delegateType / bindType (用于事件类型的调整)
setup (在某一种事件第一次绑定时调用)
add (在事件绑定时调用)
remove (在解除事件绑定时调用)
teardown (在所有事件绑定都被解除时调用)
trigger (在内部trigger事件的时候调用)
noBubble
_default
handle (在实际触发事件时调用)
preDispatch (在实际触发事件前调用)
postDispatch (在实际触发事件后调用)

在适配工作完成时,会产生一个handleObj对象,这个对象包含了所有在事件实际被触发是所需的所有参数

 

采用自定义事件或者浏览器接口绑定事件

if (!special.setup || special.setup.call(elem, data, namespaces, eventHandle) === false) {
						if (elem.addEventListener) {
							elem.addEventListener(type, eventHandle, false);
						}
					}

通过add函数,数据缓存对象就填充完毕了,看看截图:

events:handleObj 

jQuery源码分析(13)-事件绑定(2)

 

handle

jQuery源码分析(13)-事件绑定(2)

 

数据缓存对象

jQuery源码分析(13)-事件绑定(2)

得出总结:

在jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : 方法中没有传递回调对象

是因为回调的句柄被关联到了elemData,也就是内部数据缓存中了

 

不难得出jQuery的事件绑定机制:

jQuery对每一个elem中的每一种事件,只会绑定一次事件处理函数(绑定这个elemData.handle),

而这个elemData.handle实际只做一件事,就是把event丢到jQuery内部的事件分发程序

 

jQuery.event.dispatch.apply( eventHandle.elem, arguments );

而不同的事件绑定,具体是由jQuery内部维护的事件列队来区分(就是那个elemData.events)

在elemData中获取到events和handle之后,接下来就需要知道这次绑定的是什么事件了

 

画了个简单流程图

jQuery源码分析(13)-事件绑定(2)

二、事件执行

1、事件的绑定执行顺序

默认的触发循序是从事件源目标元素也就是event.target指定的元素,一直往上冒泡到document或者body,途经的元素上如果有对应的事件都会被依次触发

如果遇到委托处理?

最后得到的结论:

元素本身绑定事件的顺序处理机制

分几种情况:

假设绑定事件元素本身是A,委派元素B.C

第一种:

A,B,C各自绑定事件, 事件按照节点的冒泡层次触发

 

第二种:

元素A本身有事件,元素还需要委派元素B.C事件

委派的元素B.C肯定是该元素A内部的,所以先处理内部的委派,最后处理本身的事件

 

第三种:

元素本身有事件,元素还需要委派事件,内部委派的元素还有自己的事件,这个有点绕

先执行B,C自己本身的事件,然后处理B,C委派的事件,最后处理A事件

先看看jQuery需要应对的几个问题:

需要处理的的问题一:事件对象不同浏览器的兼容性

event 对象是 JavaScript 中一个非常重要的对象,用来表示当前事件。event 对象的属性和方法包含了当前事件的状态。

当前事件,是指正在发生的事件;状态,是与事件有关的性质,如 引发事件的DOM元素、鼠标的状态、按下的键等等。

event 对象只在事件发生的过程中才有效。

浏览器的实现差异:

获取event对象

  • 在 W3C 规范中,event 对象是随事件处理函数传入的,Chrome、FireFox、Opera、Safari、IE9.0及其以上版本都支持这种方式;
  • 但是对于 IE8.0 及其以下版本,event 对象必须作为 window 对象的一个属性。
  • 在遵循 W3C 规范的浏览器中,event 对象通过事件处理函数的参数传入。
  • event的某些属性只对特定的事件有意义。比如,fromElement 和 toElement 属性只对 onmouseover 和 onmouseout 事件有意义。

jQuery对事件的对象的兼容问题单独抽象出一个类,用来重写这个事件对象。

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'] === {};

 jQuery.event.fix() 源码分析:

//,兼容性问题处理,fix修正Event对象
/*
IE的event在是在全局的window下, 而mozilla的event是事件源参数传入到回调函数中。
还有很多的事件处理方式也一样。JQuery提供了一个 event的兼容类方案,
jQuery.event.fix 对浏览器的差异性进行包装处理。
*/
fix: function(event) {
	if (event[jQuery.expando]) {
		return event;
	}

	// Create a writable copy of the event object and normalize some properties
	var i, prop, copy,
		type = event.type,
		originalEvent = event,
		fixHook = this.fixHooks[type];

	if (!fixHook) {
		//扩展事件属性
		this.fixHooks[type] = fixHook =
			rmouseEvent.test(type) ? this.mouseHooks :
			rkeyEvent.test(type) ? this.keyHooks : {};
	}
	//私有属性与公有属性拼接
	copy = fixHook.props ? this.props.concat(fixHook.props) : this.props;
	//将浏览器原生的Event的属性赋值到新创建的jQuery.Event对象中去
	/*
	event就是对原生事件对象的一个重写了,为什么要这样,JQuery要增加自己的处理机制呗,
	这样更灵活,而且还可以传递data数据,也就是用户自定义的数据
	*/
	event = new jQuery.Event(originalEvent);

	i = copy.length;
	//jQuery自己写了一个基于native event的Event对象,
	//并且把copy数组中对应的属性从native event中复制到自己的Event对象中
	while (i--) {
		prop = copy[i];
		event[prop] = originalEvent[prop];
	}

	// Support: Cordova 2.5 (WebKit) (#13255)
	// All events should have a target; Cordova deviceready doesn't
	if (!event.target) {
		event.target = document;
	}

	// Support: Safari 6.0+, Chrome < 28
	// Target should not be a text node (#504, #13143)
	if (event.target.nodeType === 3) {
		event.target = event.target.parentNode;
	}
	//放一个钩子,调用fixHook.fitler方法用以纠正一些特定的event属性
	//最后返回这个“全新的”Event对象
	return fixHook.filter ? fixHook.filter(event, originalEvent) : event;
},

其中,调用了jQuery.Event构造函数重写event对象:

//evevt对象构造函数
jQuery.Event = function(src, props) {
	// Allow instantiation without the 'new' keyword
	if (!(this instanceof jQuery.Event)) {
		return new jQuery.Event(src, props);
	}

	// Event object
	if (src && src.type) {
		this.originalEvent = src;
		this.type = src.type;

		// Events bubbling up the document may have been marked as prevented
		// by a handler lower down the tree; reflect the correct value.
		this.isDefaultPrevented = (src.defaultPrevented ||
			src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse;

		// Event type
	} else {
		this.type = src;
	}

	// Put explicitly provided properties onto the event object
	if (props) {
		jQuery.extend(this, props);
	}

	// Create a timestamp if incoming event doesn't have one
	this.timeStamp = src && src.timeStamp || jQuery.now();

	// Mark it as fixed
	this[jQuery.expando] = true;
};

// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
//定义在原型上的方法
jQuery.Event.prototype = {
	isDefaultPrevented: returnFalse,
	isPropagationStopped: returnFalse,
	isImmediatePropagationStopped: returnFalse,
	//重写了preventDefault,stopPropagation,stopImmediatePropagation等接口
	preventDefault: function() {
		var e = this.originalEvent;
		//唯一的处理就是增加了一个状态机用来记录,当前是否调用过这个方法
		this.isDefaultPrevented = returnTrue;

		if (e && e.preventDefault) {
			e.preventDefault();
		}
	},
	stopPropagation: function() {
		var e = this.originalEvent;

		this.isPropagationStopped = returnTrue;

		if (e && e.stopPropagation) {
			e.stopPropagation();
		}
	},
	stopImmediatePropagation: function() {
		this.isImmediatePropagationStopped = returnTrue;
		this.stopPropagation();
	}
};

总的来说jQuery.event.fix干的事情:

  • 将原生的事件对象 event 修正为一个新的可写event 对象,并对该 event 的属性以及方法统一接口
  • 该方法在内部调用了 jQuery.Event(event) 构造函数

需要处理的的问题二:数据缓存

jQuery.cache 实现注册事件处理程序的存储,实际上绑定在 DOM元素上的事件处理程序只有一个,即 jQuery.cache[elem[expando]].handle 中存储的函数。

所以只要在elem中取出当对应的prop编号去缓存中找到相对应的的事件句柄就行。这个简单了,数据缓存本来就提供接口

handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [],

事件句柄拿到了,是不是立刻执行呢?当然不可以,委托还没处理呢?

需要处理的的问题三:区分事件类型,组成事件队列

事件的核心的处理来了,委托的重点

如何把回调句柄定位到当前的委托元素上面,如果有多个元素上绑定事件回调要如何处理

做这个操作之前,根据冒泡的原理,我们是不是应该把每一个节点层次的事件给规划出来,每个层次的依赖关系?

所以jQuery引入了jQuery.event.handlers用来区分普通事件与委托事件,形成一个有队列关系的组装事件处理包{elem, handlerObjs}的队列。

在最开始引入add方法中增加delegateCount用来记录是否委托数,通过传入的selector判断,此刻就能派上用场了

先判断下是否要处理委托,找到委托的句柄。jQuery.event.handlers源码解析:

/*
 处理 事件处理器 针对事件委托和原生事件(例如"click")绑定 区分对待
 事件委托从队列头部推入,而普通事件绑定从尾部推入,通过记录delegateCount来划分,委托(delegate)绑定和普通绑定。
 * @param {Object} event:jQuery.Event事件对象
 * @param {Object} handlers :事件处理程序
 * @return {Object} 返回事件处理器 队列
 */
 //组装事件处理器队列
handlers: function(event, handlers) {
	var i, matches, sel, handleObj,
		handlerQueue = [],
		delegateCount = handlers.delegateCount,
		cur = event.target;

	// Find delegate handlers
	// Black-hole SVG <use> instance trees (#13180)
	// Avoid non-left-click bubbling in Firefox (#3861)
	// 如果有delegateCount,代表该事件是delegate类型的绑定
	// 找出所有delegate的处理函数列队
	// 火狐浏览器右键或者中键点击时,会错误地冒泡到document的click事件,并且stopPropagation也无效
	if (delegateCount && cur.nodeType && (!event.button || event.type !== "click")) {

		// 遍历元素及元素父级节点
		for (; cur !== this; cur = cur.parentNode || this) {

			// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
			//不处理元素为disabled的click事件
			if (cur.disabled !== true || event.type !== "click") {
				// 开始组装符合要求的事件处理对象
				matches = [];
				for (i = 0; i < delegateCount; i++) {
					handleObj = handlers[i];

					// Don't conflict with Object.prototype properties (#13203)
					// 选择器,用于过滤
					sel = handleObj.selector + " ";

					if (matches[sel] === undefined) {
						// 如果matches上没有绑定该选择器数量
						// 得出选择器数量,并赋值
						matches[sel] = handleObj.needsContext ?
							jQuery(sel, this).index(cur) >= 0 :
							jQuery.find(sel, this, null, [cur]).length;
					}
					if (matches[sel]) {
						matches.push(handleObj);
					}
				}
				if (matches.length) {
					handlerQueue.push({
						elem: cur,
						handlers: matches
					});
				}
			}
		}
	}

	// Add the remaining (directly-bound) handlers
	if (delegateCount < handlers.length) {
		handlerQueue.push({
			elem: this,
			handlers: handlers.slice(delegateCount)
		});
	}

	return handlerQueue;
},

总的来说jQuery.event.handlers干的事情:

将有序地返回当前事件所需执行的所有事件处理程序。

这里的事件处理程序既包括直接绑定在该元素上的事件处理程序,也包括利用冒泡机制委托在该元素的事件处理程序(委托机制依赖于 selector)。

在返回这些事件处理程序时,委托的事件处理程序相对于直接绑定的事件处理程序在队列的更前面,委托层次越深,该事件处理程序则越靠前。

返回的结果是 [{elem: currentElem, handlers: handlerlist}, ...] 。

2、add方法中用到的dispatch事件分发器

源码解析:

//分派(执行)事件处理函数
dispatch: function(event) {

	// Make a writable jQuery.Event from the native event object
	// 通过原生的事件对象创建一个可写的jQuery.Event对象
	event = jQuery.event.fix(event);

	var i, j, ret, matched, handleObj,
		handlerQueue = [],
		args = core_slice.call(arguments),
		handlers = (data_priv.get(this, "events") || {})[event.type] || [],
		special = jQuery.event.special[event.type] || {};

	// Use the fix-ed jQuery.Event rather than the (read-only) native event
	args[0] = event;
	// 事件的触发元素
	event.delegateTarget = this;

	// Call the preDispatch hook for the mapped type, and let it bail if desired
	if (special.preDispatch && special.preDispatch.call(this, event) === false) {
		return;
	}

	// Determine handlers
	handlerQueue = jQuery.event.handlers.call(this, event, handlers);

	// Run delegates first; they may want to stop propagation beneath us
	i = 0;
	// 遍历事件处理器队列{elem, handlerObjs}(取出来则对应一个包了),且事件没有阻止冒泡
	while ((matched = handlerQueue[i++]) && !event.isPropagationStopped()) {
		event.currentTarget = matched.elem;

		j = 0;
		// 如果事件处理对象{handleObjs}存在(一个元素可能有很多handleObjs),且事件不需要立刻阻止冒泡
		while ((handleObj = matched.handlers[j++]) && !event.isImmediatePropagationStopped()) {

			// Triggered event must either 1) have no namespace, or
			// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
			// Triggered event must either 1) have no namespace, or
			// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
			// 触发的事件必须满足其一:
			// 1) 没有命名空间
			// 2) 有命名空间,且被绑定的事件是命名空间的一个子集
			if (!event.namespace_re || event.namespace_re.test(handleObj.namespace)) {

				event.handleObj = handleObj;
				event.data = handleObj.data;

				// 尝试通过特殊事件获取处理函数,否则使用handleObj中保存的handler(所以handleObj中还保存有handler(事件处理函数))
				// handleObj.origType 定义的事件类型
				// handleObj.handler 事件处理函数
				// 终于到这里了,开始执行事件处理函数
				ret = ((jQuery.event.special[handleObj.origType] || {}).handle || handleObj.handler)
					.apply(matched.elem, args);

				// 检测是否有返回值存在
				if (ret !== undefined) {
					// 如果处理函数返回值是false,则阻止冒泡,阻止默认动作
					if ((event.result = ret) === false) {
						event.preventDefault();
						event.stopPropagation();
					}
				}
			}
		}
	}

	// Call the postDispatch hook for the mapped type
	if (special.postDispatch) {
		special.postDispatch.call(this, event);
	}

	return event.result;
},

dispatch事件分发器处理步骤:

  1. 事件句柄缓存读取  data_priv.get
  2. 事件对象兼容       jQuery.event.fix
  3. 区分事件类型,组成事件队列  jQuery.event.handlers
  4. 对handlerQueue的筛选

对handlerQueue的筛选流程:

1 最开始就分析的事件的执行顺序,所以handlerQueue完全是按照事件的顺序排列的,委托在前,本身的事件在后面

2 产生的事件对象其实只有一份,通过jQuery.Event构造出来的event

  在遍历handlerQueue的时候修改了

  事件是绑定在父节点上的,所以此时的目标节点要通过替换,还有相对应的传递的数据,与处理句柄

  event.currentTarget = matched.elem;

  event.handleObj = handleObj;

  event.data = handleObj.data;

3 执行事件句柄

   ret = ((jQuery.event.special[handleObj.origType] || {}).handle || handleObj.handler).apply(matched.elem, args);  

4 如果有返回值 比如return false 

  系统就调用

  event.preventDefault();
  event.stopPropagation();

根据上面的分析我们就能很好的分析出on的执行流程了

在p1上绑定了自身事件,同事绑定了委托事件到li a p上都触发,然后都调用同一个回调处理

var p1 = $('#p1')

p1.on('click',function(){
	console.log('灰')
})

p1.on('click','li,a,p',function(e){
   console.log(e)
})

处理的流程:

  1. 同一节点事件需要绑2次,各处理各的流程,写入数据缓存elemData
  2. 这里要注意个问题,同一个节点上绑定多个事件,这个是在jQuery初始化绑定阶段就优化掉的了,所以触发时只会执行一次回调指令
  3. 触发节点的时候,先包装兼容事件对象,然后取出对应的elemData
  4. 遍历绑定事件节点上的delegateCount数,分组事件
  5. delegate绑定从队列头部推入,而普通绑定从尾部推入,形成处理的handlerQueue
  6. 遍历handlerQueue队列,根据判断是否isPropagationStopped,isImmediatePropagationStopped来处理对应是否执行
  7. 如果reuturn false则默认调用 event.preventDefault(); event.stopPropagation();

使用jQuery处理委托的优势?

jQuery 事件委托机制相对于浏览器默认的委托事件机制而言,其优势在于委托的事件处理程序在执行时,其内部的 this 指向发出委托的元素(即满足 selector 的元素),而不是被委托的元素,jQuery 在内部认为该事件处理程序还是绑定在那个发出委托的元素上,因此,如果开发人员在这个事件程序中中断了事件扩散—— stopPropagation,那么后面的事件将不能执行。

三、自定义事件

上章讲到trigger()和triggerHandler()都是通过调用jQuery.event.trigger()函数实现,jQuery.event.trigger()函数源码解析:

/**
模拟事件触发,为了让事件模型在各浏览器上表现一致 (并不推荐使用)
* @param {Object} event 事件对象 (原生Event事件对象将被转化为jQuery.Event对象)
* @param {Object} data 自定义传入到事件处理函数的数据
* @param {Object} elem HTML Element元素
* @param {Boolen} onlyHandlers 是否不冒泡 true 表示不冒泡  false表示冒泡        
*/
trigger: function (event, data, elem, onlyHandlers) {
	var handle, ontype, cur,
		bubbleType, special, tmp, i,
		eventPath = [elem || document],// 需要触发事件的所有元素队列
		type = core_hasOwn.call(event, "type") ? event.type : event,// 指定事件类型
		 // 事件是否有命名空间,有则分割成数组
		namespaces = core_hasOwn.call(event, "namespace") ? event.namespace.split(".") : [];

	cur = tmp = elem = elem || document;

	// Don't do events on text and comment nodes
	// 对于text和comment节点不进行事件处理
	if (elem.nodeType === 3 || elem.nodeType === 8) {
		return;
	}

	// focus/blur morphs to focusin/out; ensure we're not firing them right now
	// 仅对focus/blur事件变种成focusin/out进行处理
	// 如果浏览器原生支持focusin/out,则确保当前不触发他们
	if (rfocusMorph.test(type + jQuery.event.triggered)) {
		return;
	}
	//第一步:命名空间的过滤
	
	// 如果type有命名空间
	if (type.indexOf(".") >= 0) {
		// Namespaced trigger; create a regexp to match event type in handle()
		// 则重新组装事件
		namespaces = type.split(".");
		type = namespaces.shift();
		namespaces.sort();
	}
	// 检测是否需要改成ontype形式 即"onclick"
	ontype = type.indexOf(":") < 0 && "on" + type;
	
	//第二步:模拟事件对象
	
	// Caller can pass in a jQuery.Event object, Object, or just an event type string            
	// jQuery.expando:检测事件对象是否由jQuery.Event生成的实例,否则用jQuery.Event改造
	event = event[jQuery.expando] ?
		event :
		new jQuery.Event(type, typeof event === "object" && event);
	// 对event预处理
	event.isTrigger = true; //开关,表示已经使用了trigger (触发器)
	event.namespace = namespaces.join(".");
	event.namespace_re = event.namespace ?
		new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)") :
		null;

	// Clean up the event in case it is being reused
	// 清除事件返回数据,以重新使用
	event.result = undefined;
	// 如果事件没有触发元素,则用elem代替
	if (!event.target) {
		event.target = elem;
	}
	
	//第三步:返回事件数据集合
	
	// Clone any incoming data and prepend the event, creating the handler arg list
	// 如果data为空,则传入处理函数的是event,否则由data和event组成
	data = data == null ?
		[event] :
		jQuery.makeArray(data, [event]);
	
	//第四步:模拟事件
	// Allow special events to draw outside the lines
	// 尝试通过特殊事件进行处理,必要时候退出函数
	special = jQuery.event.special[type] || {};
	if (!onlyHandlers && special.trigger && special.trigger.apply(elem, data) === false) {
		return;
	}

	// 模拟事件冒泡
	// trigger与triggerHandler的本质区别实现在这里了
	// 如果需要冒泡,特殊事件不需要阻止冒泡,且elem不是window对象
	// onlyHandlers为true 表示不冒泡
	if (!onlyHandlers && !special.noBubble && !jQuery.isWindow(elem)) {

		// 冒泡时是否需要转成别的事件(用于事件模拟)
		bubbleType = special.delegateType || type;

		// 如果不是变形来的foucusin/out事件
		if (!rfocusMorph.test(bubbleType + type)) {
			// 则定义当前元素师父节点
			cur = cur.parentNode;
		}
		// 遍历自身及所有父节点
		for (; cur; cur = cur.parentNode) {
			eventPath.push(cur);  // 推入需要触发事件的所有元素队列
			tmp = cur; // 存一下循环中最后一个cur
		}

		// Only add window if we got to document (e.g., not plain obj or detached DOM)
		// 如果循环中最后一个cur是document,那么事件是需要最后触发到window对象上的
		// 将window对象推入元素队列
		if (tmp === (elem.ownerDocument || document)) {
			eventPath.push(tmp.defaultView || tmp.parentWindow || window);
		}
	}
	
	//第五步:处理事件,遍历每个节点,取出对应节点上的事件句柄,并确保事件不需要阻止冒泡
	
	// Fire handlers on the event path
	// 触发所有该事件对应元素的事件处理器
	i = 0;
	// 遍历所有元素,并确保事件不需要阻止冒泡
	while ((cur = eventPath[i++]) && !event.isPropagationStopped()) {

		// 先确定事件绑定类型是delegateType还是bindType
		event.type = i > 1 ?
			bubbleType :
			special.bindType || type;

		// jQuery handler
		// 检测缓存中该元素对应事件中包含事件处理器,
		// 有则取出主处理器(jQuery handle)来控制所有分事件处理器
		handle = (jQuery._data(cur, "events") || {})[event.type] && jQuery._data(cur, "handle");
		// 如果主处理器(jQuery handle)存在
		if (handle) {
			// 触发处理器
			handle.apply(cur, data);
		}

		// Native handler
		// 取出原生事件处理器elem.ontype (比如click事件就是elem.onclick)              
		handle = ontype && cur[ontype];
		// 如果原生事件处理器存在,检测需不需要阻止事件在浏览器上的默认动作
		if (handle && jQuery.acceptData(cur) && handle.apply && handle.apply(cur, data) === false) {
			event.preventDefault();
		}
	}
	// 保存事件类型,因为这时候事件可能变了
	event.type = type;

	// If nobody prevented the default action, do it now
	// 如果不需要阻止默认动作,立即执行
	if (!onlyHandlers && !event.isDefaultPrevented()) {
		// 尝试通过特殊事件触发默认动作
		if ((!special._default || special._default.apply(elem.ownerDocument, data) === false) &&
			!(type === "click" && jQuery.nodeName(elem, "a")) && jQuery.acceptData(elem)) {

			// Call a native DOM method on the target with the same name name as the event.
			// Can't use an .isFunction() check here because IE6/7 fails that test.
			// Don't do default actions on window, that's where global variables be (#6170)

			// 调用一个原生的DOM方法具有相同名称的名称作为事件的目标。
			// 例如对于事件click,elem.click()是触发该事件
			// 并确保不对window对象阻止默认事件
			if (ontype && elem[type] && !jQuery.isWindow(elem)) {

				// Don't re-trigger an onFOO event when we call its FOO() method
				// 防止我们触发FOO()来触发其默认动作时,onFOO事件又触发了
				tmp = elem[ontype];
				// 清除掉该事件监听
				if (tmp) {
					elem[ontype] = null;
				}

				// Prevent re-triggering of the same event, since we already bubbled it above

				// 当我们已经将事件向上起泡时,防止相同事件再次触发
				jQuery.event.triggered = type;
				try {
					// 触发事件
					elem[type]();
				} catch (e) {
					// IE<9 dies on focus/blur to hidden element (#1486,#12518)
					// only reproducible on winXP IE8 native, not IE9 in IE8 mode
				}
				// 完成清除标记
				jQuery.event.triggered = undefined;
				// 事件触发完了,可以把监听重新绑定回去
				if (tmp) {
					elem[ontype] = tmp;
				}
			}
		}
	}

	return event.result;
},

trigger的几种常见用法:

1、常用模拟

在jQuery中,可以使用trigger()方法完成模拟操作。例如可以使用下面的代码来触发id为btn按钮的click事件。

$("#btn").trigger("click");

2、触发自定义事件

trigger()方法不仅能触发浏览器支持的具有相同名称的事件,也可以触发自定义名称的事件

3、传递数据

trigger(tpye[,datea])方法有两个参数,第一个参数是要触发的事件类型,第二个单数是要传递给事件处理函数的附加数据,以数组形式传递。通常可以通过传递一个参数给回调函数来区别这次事件是代码触发的还是用户触发的。

4、执行默认操作

triger()方法触发事件后,会执行浏览器默认操作。

例如:

$("input").trigger("focus");

以上代码不仅会触发为input元素绑定的focus事件,也会使input元素本身得到焦点(浏览器默认操作)。

如果只想触发绑定的focus事件,而不想执行浏览器默认操作,可以使用jQuery中另一个类似的方法-triggerHandler()方法。

$("input").triggerHandler("focus");

该方法会触发input元素上绑定的特定事件,同时取消浏览器对此事件的默认操作,即文本框指触发绑定的focus事件,不会得到焦点。

四、自定义事件

jQuery.event.special方法

这个方法在event.add,event.dispatch等几个事件的处理地方都会被调用到,jQuert.event.special 对象用于某些事件类型的特殊行为和属性。换句话说就是某些事件不是大众化的的事件,不能一概处理,比如 load 事件拥有特殊的 noBubble 属性,可以防止该事件的冒泡而引发一些错误。所以需要单独针对处理,但是如果都写成判断的形式,显然代码结构就不合理了,而且不方便提供给用户自定义扩展。

大体上针对9种事件,不同情况下处理hack,我们具体分析下焦点事件兼容冒泡处理,处理大同小异

针对focusin/ focusout 事件jQuery.event.special扩充2组处理机制,

special.setup方法主要是来在Firefox中模拟focusin和focusout事件的,因为各大主流浏览器只有他不支持这两个事件。

由于这两个方法支持事件冒泡,所以可以用来进行事件代理

// Attach a single capturing handler while someone wants focusin/focusout
var attaches = 0,
	handler = function( event ) {
		jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );
	};

jQuery.event.special[ fix ] = {
	setup: function() {
		if ( attaches++ === 0 ) {
			document.addEventListener( orig, handler, true );
		}
	},
	teardown: function() {
		if ( --attaches === 0 ) {
			document.removeEventListener( orig, handler, true );
		}
	}
};

前面的分析我们就知道通过事件最终都是通过add方法绑定的,也就是addEventListener方法绑定的,但是在add方法之前会有一个过滤分支

if (!special.setup || special.setup.call(elem, data, namespaces, eventHandle) === false) {
	if (elem.addEventListener) {
		elem.addEventListener(type, eventHandle, false);
	}
}

可见对focusin/ focusout 的处理,没有用通用的方法,而且是直接用的special.setup中的绑定

因为火狐不支持focusin/ focusout事件,所以要找个所有浏览器都兼容类似事件,对了那就是focus/blur,但是focus/blur不能冒泡,所以利用 jQuery.event.simulate方法将捕获模拟出冒泡。

simulate: function(type, elem, event, bubble) {
            // Piggyback on a donor event to simulate a different one.
            // Fake originalEvent to avoid donor's stopPropagation, but if the
            // simulated event prevents default then we do the same on the donor.
			//重写事件
            var e = jQuery.extend(
                new jQuery.Event(),
                event, {
                    type: type,
                    isSimulated: true,
                    originalEvent: {}
                }
            );
			//如果要冒泡
            if (bubble) {
				// 利用jQuery.event.trigger模拟触发事件
                jQuery.event.trigger(e, null, elem);
            } else {
				// 否则利用jQuery.event.dispatch来执行处理
                jQuery.event.dispatch.call(elem, e);
            }
			// 如果需要阻止默认操作,则阻止
            if (e.isDefaultPrevented()) {
                event.preventDefault();
            }
        }

可以看到focusin/ focusout 可冒泡事件实现原理是

1 focusin 事件添加事件处理程序时,jQuery 会在 document 上会添加 handler 函数

2 在事件捕获阶段监视特定元素的 focus/ blur 动作,捕获行为发生在 document 对象上,这样才能有效地实现所有元素都能可以冒泡的事件。

3 程序监视到存在 focus/ blur 行为,就会触发绑定在 document 元素上的事件处理程序,该事件处理程序在内部调用 simulate 逻辑触发事件冒泡,以实现我们希望的可以冒泡事件。

之后利用jQuery.event.trigger模拟触发事件,把从target-document的元素都过滤出来,分析每个节点上是否绑定了事件句柄,依次处理,按照一定的规范,比如是否有事件阻止之类的,这里就不再重复分析了

五、总结

  1. jQuery为统一原生Event对象而封装的jQuery.Event类,封装了preventDefault,stopPropagation,stopImmediatePropagation原生接口,可以直接捕获到用户的行为
  2. 由核心组件 jQuery.cache 实现注册事件处理程序的存储,实际上绑定在 DOM元素上的事件处理程序只有一个,即 jQuery.cache[elem[expando]].handle 中存储的函数,该函数在内部调用 jQuery.event.dispatch(event) 实现对该DOM元素特定事件的缓存的访问,并依次执行这些事件处理程序。
  3. jQuery.event.add(elem, types, handler, data, selector) 方法用于给特定elem元素添加特定的事件 types([type.namespace, type.namespace, ...])的事件处理程序 handler, 通过第四个参数 data 增强执行当前 handler 事件处理程序时的 $event.data 属性,以提供更灵活的数据通讯,而第五个元素用于指定基于选择器的委托事件
  4. namespace 命名空间机制,namespace 机制可以对事件进行更为精细的控制,开发人员可以指定特定空间的事件,删除特定命名空间的事件,以及触发特定命名空间的事件。这使得对事件处理机制的功能更加健
  5. jQuert.event.special 对象用于某些事件类型的特殊行为和属性。比如 load 事件拥有特殊的 noBubble 属性,可以防止该事件的冒泡而引发一些错误。总的来说,有这样一些方法和属性:
  6. jQuery.event.simulate(type, elem, event, bubble)模拟事件并立刻触发方法,可用于在DOM元素 elem 上模拟自定义事件类型 type,参数 bubble用于指定该事件是否可冒泡,event 参数表示 jQuery 事件对象 $event。 模拟事件通过事件对象的isSimulated属性为 true 表示这是模拟事件。该方法内部调用 trigger() 逻辑 或 dispatch() 逻辑立刻触发该模拟事件。该方法主要用于修正浏览器事件的兼容性问题,比如模拟出可冒泡的 focusin/ focusout 事件,修正IE中 change 事件的不可冒泡问题,修正IE中 submit事件不可冒泡问题
  7. jQuery.event.dispatch(event) 方法在处理事件委托机制时,依赖委托节点在DOM树的深度安排优先级,委托的DOM节点层次越深,其执行优先级越高。而其对于stopPropagation的处理有些特殊,在事件委托情况下并不一定会调用绑定在该DOM元素上的该类型的所有事件处理程序,而依赖于委托的事件处理程序的执行结果,如果低层委托的事件处理程序声明了停止冒泡,那么高层委托的事件以及自身绑定事件就不会被执行,这拓展了 DOM 委托机制的功能。
  8. jQuery.event.trigger(event | type, data, elem, onlyHandlers) 方法提供开发人员以程序方式触发特定事件的接口,该方法的第一个参数可以是 $event/ event 对象 ,也可以是某个事件类型的字符串 type; 第二个参数 data 用于扩展该事件触发时事件处理程序的参数规模,用于传递一些必要的信息。 elem参数表示触发该事件的DOM元素;最后该方法在默认情况下,其事件会冒泡,并且在有默认动作的情况下执行默认行为,但是如果指定了 onlyHandlers 参数,该方法只会触发绑定在该DOM元素上的事件处理程序,而不会引发冒泡和默认动作,也不会触发特殊的 trigger 行为。