1. 程式人生 > >javascript事件監聽機制(二)——jquery的Event物件

javascript事件監聽機制(二)——jquery的Event物件

jQuery封裝了瀏覽器的事件監聽方法,相容了各個瀏覽器的區別,對外提供一套適合於jquery物件的事件監聽介面。Event物件的核心方法主要有三個,add,remove和trigger。

elem對於事件的維護,是通過在elem的內部空間裡(_data訪問的),用一個events物件來實現的

{
  events:{'click':handleObj, 'focusin':handleObj...},
  handle:function(e){this.elem = elem; 
                return function(e){bind(this.elem,arguments);}}
}

events物件用於維護該elem監聽的全部事件。在events[type]下,是監聽改時間的handle“材料包”。結構為:
handleObj = jQuery.extend({
				type: type, // 傳入的型別,通過special適配之後可能變為其他的型別,如focus,轉為focusin,blur轉為focusout
				origType: tns[1],//初始型別 
				data: data, //傳入的資料
				handler: handler, //callback們(通過extend實現的。再次繫結的時候,把新的selector和handler放到對應的位置)
				guid: handler.guid,//確保該elem對於這一類事件只有一個id
				selector: selector,//selector們
				namespace: namespaces.join(".") // 監聽空間
			}, handleObjIn );

handler實質上是一個bind操作,執行回撥,context為elem

三個操作都是圍繞著這個資料結構進行操作。以下依次做分析:

add:

操作流程:

   step1:判斷節點型別,文字節點和屬性節點不能新增事件,沒有提供型別,回撥,或者elem不是jq物件的話,直接返回
// Don't attach events to noData or text/comment nodes (allow plain objects tho)
		if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) {
			return; //預判,文字節點,屬性節點不能附加事件
		}
step2:拆分handler,獲取selector和callback。並將其整合為一個handleObjIn,用於和已有的handleObj做合併
if ( handler.handler ) {
			handleObjIn = handler;
			handler = handleObjIn.handler;
			selector = handleObjIn.selector;
		}

step3:構造該elem的events物件和handle。定義handle包裹為一個dispatch,本質上是一個bind函式
events = elemData.events;
		if ( !events ) {
			elemData.events = events = {}; //初始化elemData.events。第一次監聽事件的情況,初始化events物件
		}
		eventHandle = elemData.handle;
		if ( !eventHandle ) { //初始化elemData.handle
			elemData.handle = eventHandle = function( e ) {
				// Discard the second event of a jQuery.event.trigger() and
				// when an event is called after a page has unloaded
				return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? //忽略trigger事件,否則呼叫dispatch方法。handle方法類似於一個代理,把真正的工作交給dispatch
					jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
					undefined;
			};
			// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
			eventHandle.elem = elem;
		}
step4:迴圈types,依次構造handlerObj
			tns = rtypenamespace.exec( types[t] ) || [];
			type = tns[1];
			namespaces = ( tns[2] || "" ).split( "." ).sort(); //拆分出來type和namespace

			// If event changes its type, use the special event handlers for the changed type
			// special In using this API, you can create custom events that do more than just execute bound event handlers when triggered—these “special” events can modify the event object passed to event handlers, trigger other entirely different events, or execute complex setup and teardown code when event handlers are bound to or unbound from elements.
			//special 不僅可以執行繫結到事件上的handler,還能修改事件物件,觸發其他不同的事件。把一些神奇的事件放到special system裡面
			special = jQuery.event.special[ type ] || {};

			// If selector defined, determine special event api type, otherwise given type
			//如果有selector 用special決定的型別,或者繫結的型別。如果special失敗用type。比如傳入focus,會轉化為focusin
			type = ( selector ? special.delegateType : special.bindType ) || type;

			// Update special based on newly reset type
			special = jQuery.event.special[ type ] || {}; //更新special 不懂為啥不直接在這裡獲取special物件

			// handleObj is passed to all event handlers
			//handler可以是一個函式,即繫結在事件上的方法
			//同時也可以是一個事件物件,也就是下面所說的handleObj,那麼如果是在jQuery的內部是可以傳遞一個事件物件過來的
			handleObj = jQuery.extend({
				type: type,
				origType: tns[1],
				data: data,
				handler: handler,
				guid: handler.guid,
				selector: selector,
				namespace: namespaces.join(".")
			}, handleObjIn );

step5:繫結事件:
handlers = events[ type ]; //獲取繫結的事件
			if ( !handlers ) {//如果還沒有繫結的話,則初始化。handlers是因為可以對一個事件上繫結多個handlers
				handlers = events[ type ] = [];
				handlers.delegateCount = 0;//事件委託數
                                //該元素首次監聽該類事件的時候,先呼叫special的setup方法,然後如果失敗了,那麼手動進行事件繫結
				// Only use addEventListener/attachEvent if the special events handler returns false
				if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { // special方法失敗之後,用add或者attach。 用load 和beforeunload事件。load用jquery提供的bindReady方法.beforeunload 需要elem是window
					// Bind the global event handler to the element
					if ( elem.addEventListener ) {
						elem.addEventListener( type, eventHandle, false );//顯示的指明false,監聽函式只在冒泡階段被觸發

					} else if ( elem.attachEvent ) {
						elem.attachEvent( "on" + type, eventHandle );
					}
				}
			}//setup總會在add之前執行
                        //呼叫special的add方法
			if ( special.add ) {//呼叫special的add方法
				special.add.call( elem, handleObj ); //把handleObj add到elem上

				if ( !handleObj.handler.guid ) {
					handleObj.handler.guid = handler.guid;
				}
			}

			// Add to the element's handler list, delegates in front
			if ( selector ) { //將handlerObj新增到elem的事件佇列裡面
				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; // jquery.event.global會維護一個數組,用於存放全部監聽的事件型別

最終執行完,events物件裡面的資料儲存結構為:

events={

  type+namespace:[handleobj1, handleobj2, handleobj3..],

  type2: [handleobj4, handleobj5...]

}

remove:

  操作流程:根據arguments提供的值,將target事件,或者事件組從events裡面剝離下來。

 step1:先獲取使用者的events物件,如果沒有,說明該物件沒有繫結任何事件。直接返回

if ( !elemData || !(events = elemData.events) ) {//如果沒有event,直接return
			return;
		}

 step2:先處理一下傳入的types們,

types = jQuery.trim( hoverHack( types || "" ) ).split(" "); //獲取傳入的所有需要刪除的types
其中hoverHack方法,目的在於將events裡面的hover轉為mouseenter mouseleave
hoverHack = function( events ) {
		return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" );
	};

在獲取完所有的型別之後,核心是一個遍歷。獲取所有的事件型別

step3: 根據type,分割事件型別type+ 名稱空間namespace。並獲取events中type對應的空間。

special = jQuery.event.special[ type ] || {}; 
type = ( selector? special.delegateType : special.bindType ) || type; //如果有selector,則找到代理的type或者bindtype
eventType = events[ type ] || []; //獲取繫結到這個事件上的一組handlers
origCount = eventType.length;
namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.|)") + "(\\.|$)") : null;
  • 獲取該型別的special物件。
  • 根據是否提供了selector,呼叫specail的型別檢查方法,或者直接是type
  • 根據type,獲取events[type]對應的handleObj陣列
step4:移除事件的操作
for ( j = 0; j < eventType.length; j++ ) {
				handleObj = eventType[ j ];// 獲取每一個handleObj
				

                                        if ( ( mappedTypes || origType === handleObj.origType ) && 
                                        // 刪除的條件是,type對的上號,名稱空間對應,selector對上的,都滿足的情況,才將其從events[type]中移除
					 ( !handler || handler.guid === handleObj.guid ) &&
					 ( !namespaces || namespaces.test( handleObj.namespace ) ) &&
					 ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) {
                                        //需要繫結的事件型別,名稱空間guid都對應的上才刪掉
					eventType.splice( j--, 1 );

					if ( handleObj.selector ) {
						eventType.delegateCount--;
					}
					if ( special.remove ) {
						special.remove.call( elem, handleObj );
					}
				}
			}

step5:最後需要判斷刪除完成之後,該型別是不是沒有事件了。如果是的話,如果有specail,則呼叫specail的teardown,執行一些之前公共操作。否則直接呼叫delete方法,將該事件型別從events中刪掉。
if ( eventType.length === 0 && origCount !== eventType.length ) {//如果該事件上沒有handlers
				if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
					jQuery.removeEvent( elem, type, elemData.handle );
				}

				delete events[ type ]; //刪掉這個屬性
			}

step6:如果刪除完了之後,該elem沒有再繫結任何事件。那麼直接將events物件從elemData中刪除
if ( jQuery.isEmptyObject( events ) ) {//這兩個操作都是刪掉events
			delete elemData.handle;

			// removeData also checks for emptiness and clears the expando if empty
			// so use it instead of delete
			jQuery.removeData( elem, "events", true ); //直接把events從elem上刪掉
		}


trigger:

這個方法主要做的事情有:

1、適配引數,找到需要觸發的event,觸發元素elem等。

 2、構造event物件。

 3、根據事件觸發的位置(捕獲/冒泡),獲取事件觸發的路徑eventPath

 4、根據eventPath,依次獲取elemData.events[type] 中的handleObj,執行handleObj中的handle。並將data作為arguments傳入。

 5、根據事件執行的結果,修正event中的屬性。並將event.result返回

step1: 修正引數

// Don't do events on text and comment nodes
		if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) {
			return;
		}

		// Event object or event type
		var cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType,
			type = event.type || event,//獲取觸發型別,可以通過obj傳入,或者直接傳入
			namespaces = [];

		// focus/blur morphs to focusin/out; ensure we're not firing them right now
		if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { //focus blur,focusin,focusout不能直接觸發。測試 focusin+focus 和focusout+blur
			return;
		}

		if ( type.indexOf( "!" ) >= 0 ) {
			// Exclusive events trigger only for the exact event (no namespaces)
			type = type.slice(0, -1);
			exclusive = true;
		}

		if ( type.indexOf( "." ) >= 0 ) {//有名稱空間的情況
			// Namespaced trigger; create a regexp to match event type in handle()
			namespaces = type.split(".");
			type = namespaces.shift();
			namespaces.sort(); //多個空間的情況
		}
                
                //直接去global上看該事件是否被監聽過,如果沒有的話,直接返回
if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { // No jQuery handlers for this event type, and it can't have inline handlersreturn;} step2:構造event物件
event = typeof event === "object" ? // 檢測event是不是一個jquery的event物件。如果不是的話,重新構造
	// jQuery.Event object
	event[ jQuery.expando ] ? event :
	// Object literal
	new jQuery.Event( type, event ) :
	// Just the event type (string)
	new jQuery.Event( type );

		event.type = type;
		event.isTrigger = true;
		event.exclusive = exclusive;
		event.namespace = namespaces.join( "." );
		event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)") : null;
		ontype = type.indexOf( ":" ) < 0 ? "on" + type : "";
// Clean up the event in case it is being reused
		event.result = undefined;
		if ( !event.target ) {
			event.target = elem;//繫結event的物件到elem
		}
                data = data != null ? jQuery.makeArray( data ) : []; //如果傳入了資料,那麼先將資料構造為array
		data.unshift( event );//將傳入的資料,繫結到event上


構造一個事件物件:

初始的構造為:

Event = {
   originalEvent:event物件,
   type:event.type即事件型別,
   timeStap: new date().getTime(),
   uid: true,
   isDefaultPrevented:function(){
      return getPreventDefault()? true:  false;
   }
}

經過trigger的加工之後,修正event.type 為type,新增isTrigger屬性為true,namespace,ontype 為type中是否有:如果沒有的話,則是ontype。event.result先定義為undefined。

step3:獲取該type的special物件。先執行trigger前的前置操作。然後再獲取事件觸發路徑eventPath。最終eventPath中的資料為二維陣列,每一項為[elem,type]。

special = jQuery.event.special[ type ] || {};
		if ( special.trigger && special.trigger.apply( elem, data ) === false ) {//獲取special,執行special的方法,如果返回false,那麼不做後續操作
			return;
		}

		// Determine event propagation path in advance, per W3C events spec (#9951)
		// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
		eventPath = [[ elem, special.bindType || type ]];//先選定好事件冒泡的順序。
		if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {// 如果該事件允許冒泡,並且elem不是window

			bubbleType = special.delegateType || type;//獲取冒泡的事件
			cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode;//type是不是focus/blur。如果是的話,那麼觸發的元素轉移到parent
			for ( old = elem; cur; cur = cur.parentNode ) {// 順著當前元素向上,將elem和bubbletype放入eventPath中
				eventPath.push([ cur, bubbleType ]);
				old = cur;
			}//先獲取路由順序

			// Only add window if we got to document (e.g., not plain obj or detached DOM)
			if ( old === (elem.ownerDocument || document) ) {
				eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]);//到document之後,把window加到最後
			}
		}
step4: 順著eventPath,依次觸發事件。
// Fire handlers on the event path
		//在事件路徑上觸發事件
		for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) {//如果沒有發生事件攔截的話,一直向上冒泡

			cur = eventPath[i][0]; // 獲取路徑上的elem 和事件型別
 			event.type = eventPath[i][1];
                        //
                        //獲取cur的elemData.event[type]上的handle
handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); if ( handle ) {handle.apply( cur, data ); // 執行handle}// Note that this is a bare JS function and not a jQuery handlerhandle = ontype && cur[ ontype ];//獲取內聯的handler // 如果該elem還有內聯的方法,執行完結果為false沒那麼阻止之後的動作if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) {event.preventDefault();//不要執行事件的預設動作}}event.type = type;

step5:依次觸發完成之後,執行收尾動作

if ( !onlyHandlers && !event.isDefaultPrevented() ) {// 如果沒有elem組織default action,那麼在結束的時候,阻止之

			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)
				// IE<9 dies on focus/blur to hidden element (#1486)
				if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") 
|| event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) 
{//如果有ontype,不是focus和blur,target可見,不是window

					// Don't re-trigger an onFOO event when we call its FOO() method
					old = elem[ ontype ];

					if ( old ) {
						elem[ ontype ] = null;
					}

					// Prevent re-triggering of the same event, since we already bubbled it above
					jQuery.event.triggered = type;//不要重複觸發同樣的事件
					elem[ type ]();
					jQuery.event.triggered = undefined;

					if ( old ) {
						elem[ ontype ] = old;
					}
				}
			}
		}

		return event.result;


  以上就是jquery事件處理機制的三個核心方法分析。之後將event的specail方法以及v8的事件處理機制。