jQuery源碼分析(13)-事件綁定(2)

事件綁定與執行的具體流程:

一、事件預綁定

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 

image

 

handle

image

 

數據緩存對象

image

得出總結:

在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之後,接下來就需要知道這次綁定的是什麼事件了

 

畫了個簡單流程圖

image

二、事件執行

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 行爲。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章