jQuery源碼學習(12)-事件綁定(1)

1、javaScript傳統的事件處理

給某一個元素綁定了一個點擊事件,傳入一個回調句柄處理:

element.addEventListener('click',doSomething,false);

但是如果頁面上有幾百個元素需要綁定,那需要綁定幾百次,這樣就可以知道傳統綁定方法存在的問題:

  • 大量的事件綁定,性能消耗,而且還需要解綁(IE會泄漏);
  • 綁定的元素必須要存在;
  • 後期生成的HTML會沒有事件綁定,需要重新綁定;
  • 語法過於繁雜。

關於jQuery提出的事件綁定的一些方法:bind()、live()、on()、delegate()。對應的解除事件綁定的函數分別是:unbind()、die()、off()、undelegate()。其中,live和delegate方法利用了事件委託機制,很好的解決了大量事件綁定的問題,所以先介紹事件委託的原理。

2、事件委託

DOM有個事件流的特性,也就是說我們在頁面上觸發節點的時候事件都會上下或者向上傳播,事件捕捉和事件冒泡。

DOM2.0模型將事件處理流程分爲三個階段:一、事件捕獲階段,二、事件目標階段,三、事件起泡階段。

模型如圖所示:


事件傳送可以分爲3個階段。

(1).在事件捕捉(Capturing)階段,事件將沿着DOM樹向下轉送,目標節點的每一個祖先節點,直至目標節點。例如,若用戶單擊了一個超鏈接,則該單擊事件將從document節點轉送到html元素,body元素以及包含該鏈接的p元素。在此過程中,瀏覽器都會檢測針對該事件的捕捉事件監聽器,並且運行這件事件監聽器。

(2)在目標(target)階段,瀏覽器在查找到已經指定給目標事件的事件監聽器之後,就會運行 該事件監聽器。目標節點就是觸發事件的DOM節點。例如,如果用戶單擊一個超鏈接,那麼該鏈接就是目標節點(此時的目標節點實際上是超鏈接內的文本節點)。

(3).在冒泡(Bubbling)階段,事件將沿着DOM樹向上轉送,再次逐個訪問目標元素的祖先節點到document節點。該過程中的每一步。瀏覽器都將檢測那些不是捕捉事件監聽器的事件監聽器,並執行它們。

利用事件傳播(這裏是冒泡)這個機制,就可以實現事件委託。

具體來說,事件委託就是事件目標自身不處理事件,而是把處理任務委託給其父元素或者祖先元素,甚至根元素(document)

舉例子:

<ul id="myLinks">
	<li id="goSomeWhere">Go somewhere</li>
	<li id="doSomeThing">Do someThing</li>
	<li id="sayHi">Say Hi</li>
</ul>

以上代碼包含三個點擊後會執行操作的列表項。按照傳統的JS做法,我們需要像下面這樣爲他們添加3個事件處理程序:

var item1=document.getElementById("goSomeWhere");
var item2=document.getElementById("doSomeThing");
var item3=document.getElementById("sayHi");
EvevtUtil.addHandler(item1,"click",function(event){
	location.href = "http://www.xx.com";
});
EvevtUtil.addHandler(item2,"click",function(event){
	document.title = "I change the document";
});
EvevtUtil.addHandler(item3,"click",function(event){
	alert("hi");
});

但是,使用事件委託,只需在DOM樹中儘量高的層次上添加一個事件處理程序:

var list = document.getElementById("myLinks");
EventUtil.addHandler(list,"click",function(event){
	event = EventUtil.getEvent(event);
	var target = EventUtil.getTarget(event);
	
	switch(target.id){
		case: "doSomeThing"
			document.title="I change the document";
			break;
		case: "goSomeWhere"
			location.href="http://www.xx.com";
			break;
		case: "sayHi"
			alert("Hi");
			break;
	}
});
使用事件委託只爲<ul>元素添加了一個onclick事件處理程序,事件目標是被單擊的列表項,他們會冒泡至父節點通過檢測ID屬性來執行對應的操作。

3、方法解析

一、bind(type,[data],function(eventObject))

bind()的作用就是在選擇到的元素上綁定特定事件類型的事件處理程序,參數含義如下:

type:事件類型,如click、change、mouseover等;

data:傳入事件處理函數的參數,通過event.data取到,可選;

function:事件處理函數,可傳入event對象,但是這裏的event是jQuery封裝的event對象,與原生的event有區別。

bind的源碼:

bind: function(types,data,fn){
    return this.on(types,null,data,fn);
}
//使用方式
$("#myol li").bind('click',getHtml);

bind的特點就是直接附加一個事件處理程序到元素上,有一個綁一個,在頁面的元素不會動態添加的時候使用它沒什麼,但是如果在列表中動態增加一個列表元素li,點擊它是沒有反應的,必須再bind一次。即,在bind綁定事件的時候,這些元素必須已經存在。爲了解決這個問題,可以使用live方法。

二、live(type,[data],fn) (已被棄用)

live參數與bind一樣,源碼如下:

live: function(types,data,fn){
  jQuery(this.context).on(types,this.selector,data,fn);
    return this;
}
//live方法並沒有將事件處理函數綁定到自己(this)身上,而是綁定到了this.context上了,即元素的限定範圍。一般元素的範圍都是document。

所以live函數將委託的事件處理程序附加到一個頁面的document元素,從而簡化了在頁面上動態添加的內容上事件處理的使用。

例如:

$('a').live('click',function(){alert("!!")});

JQuery把alert函數綁定到$(document)元素上,並使用’click’和’a’作爲參數。任何時候只要有事件冒泡到document節點上,它就查看該事件是否是一個click事件,以及該事件的目標元素與’a’這一CSS選擇器是否匹配,如果都是的話,則執行函數。但是使用live方法還存在以下問題:

  • 在調用 .live() 方法之前,jQuery 會先獲取與指定的選擇器匹配的元素,這一點對於大型文檔來說是很花費時間的。
  • 不支持鏈式寫法。例如,$("a").find(".offsite, .external").live( ... ); 這樣的寫法是不合法的,並不能像期待的那樣起作用。
  • 由於所有的 .live() 事件被添加到 document 元素上,所以在事件被處理之前,可能會通過最長最慢的那條路徑之後才能被觸發。
  • 在移動 iOS (iPhone, iPad 和 iPod Touch) 上,對於大多數元素而言,click 事件不會冒泡到文檔 body 上,並且如果不滿足如下情況之一,就不能和 .live() 方法一起使用:
    1. 使用原生的可被點擊的元素,例如, a 或 button,因爲這兩個元素可以冒泡到 document
    2. 在 document.body 內的元素使用 .on() 或 .delegate() 進行綁定,因爲移動 iOS 只有在 body 內才能進行冒泡。
    3. 需要 click 冒泡到元素上才能應用的 CSS 樣式 cursor:pointer (或者是父元素包含document.documentElement)。但是依需注意的是,這樣會禁止元素上的複製/粘貼功能,並且當點擊元素時,會導致該元素被高亮顯示。
  • 在事件處理中調用 event.stopPropagation() 來阻止事件處理被添加到 document 之後的節點中,是效率很低的。因爲事件已經被傳播到 document 上。
  • .live() 方法與其它事件方法的相互影響是會令人感到驚訝的。例如,$(document).unbind("click") 會移除所有通過 .live() 添加的 click 事件!

三、delegate()

爲了解決live存在的上述問題,jQuery引入了一個新方法delegate(),其把處理程序綁定到具體的元素而非document這一根上。源碼:

delegate: function(selector,types,data,fn){
	return this.on(types,selector,data,fn);
}

參數多了一個selector,用來指定觸發事件的目標元素。

例如:

$('#element').delegate('a','click',function(){
	alert("!!!");
});
使用click事件和'a'這一css選擇器作爲參數把alert函數綁定到了元素'#element'上。任何時候只要有事件冒泡到$(‘#element)上,它就查看該事件是否是click事件,以及該事件的目標元素是否與CCS選擇器相匹配。如果兩種檢查的結果都爲真的話,它就執行函數。

四、.on( events [, selector ] [, data ], handler(eventObject) )

events:事件名

selector : 一個選擇器字符串,用於過濾出被選中的元素中能觸發事件的後代元素

data :當一個事件被觸發時,要傳遞給事件處理函數的

handler:事件被觸發時,執行的函數

所有delegate(),live(),bind()方法內部都是調用的on方法。 

undelegate(),unlive(),unbind()方法內部都是調用的off方法。

on方法源碼解析:

//on方法實質只完成一些參數調整的工作,而實際負責事件綁定的是其內部jQuery.event.add方法。
on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
	var type, origFn;

	// Types can be a map of types/handlers
	if ( typeof types === "object" ) {
		// ( types-Object, selector, data )
		if ( typeof selector !== "string" ) {
			// ( types-Object, data )
			data = data || selector;
			selector = undefined;
		}
		// 遍歷types對象,針對每一個屬性綁定on()方法
		// 將types[type]作爲fn傳入
		for ( type in types ) {
			this.on( type, selector, data, types[ type ], one );
		}
		return this;
	}

	// 參數修正
	// jQuery這種參數修正的方法很好
	// 可以兼容多種參數形式
	// 可見在靈活調用的背後做了很多處理
	if ( data == null && fn == null ) {
		// ( types, fn )
		fn = selector;
		data = selector = undefined;
	} else if ( fn == null ) {
		if ( typeof selector === "string" ) {
			// ( types, selector, fn )
			fn = data;
			data = undefined;
		} else {
			// ( types, data, fn )
			fn = data;
			data = selector;
			selector = undefined;
		}
	}
	if ( fn === false ) {
		// fn傳入false時,阻止該事件的默認行爲
		// function returnFalse() {return false;}
		fn = returnFalse;
	} else if ( !fn ) {
		return this;
	}

	// one()調用on()
	if ( one === 1 ) {
		origFn = fn;
		fn = function( event ) {
			// Can use an empty set, since event contains the info
			// 用一個空jQuery對象,這樣可以使用.off方法,
			// 並且event帶有remove事件需要的信息
			jQuery().off( event );
			return origFn.apply( this, arguments );
		};
		// Use same guid so caller can remove using origFn
		// 事件刪除依賴於guid
		fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
	}

	// 這裏調用jQuery的each方法遍歷調用on()方法的jQuery對象
	// 如$('li').on(...)則遍歷每一個li傳入add()
	// 推薦使用$(document).on()或者集合元素的父元素
	return this.each( function() {
		jQuery.event.add( this, types, fn, data, selector );
	});
},

例如:

var body = $('body')
body.on('click','p',function(){
    console.log(this)
})

用on方法給body上綁定一個click事件,冒泡到p元素的時候纔出發回調函數

這裏大家需要明確一點:每次在body上點擊其實都會觸發事件,但是隻目標爲p元素的情況下才會觸發回調handler

關於上述四種方法的總結:

在下列情況下,應該使用.live()或.delegate(),而不能使用.bind():

  • 爲DOM中的很多元素綁定相同事件;
  • 爲DOM中尚不存在的元素綁定事件;

用.bind()的代價是非常大的,它會把相同的一個事件處理程序hook到所有匹配的DOM元素上
不要再用.live()了,它已經不再被推薦了,而且還有許多問題
.delegate()會提供很好的方法來提高效率,同時我們可以添加一事件處理方法到動態添加的元素上
我們可以用.on()來代替上述的3種方法

不足點也是有的:

  • 並非所有的事件都能冒泡,如load, change, submit, focus, blur
  • 加大管理複雜。
  • 不好模擬用戶觸發事件

4、事件體系結構

4.1 整個事件的API有:


4.2 事件結構

所有的函數添加事件都會進入jQuery.event.add函數。該函數有兩個主要功能:添加事件、附加很多事件相關信息。下章講解源碼。

用實例來說明jQuery的事件結構:

<div id="#center"></div>

<script>
  function dohander(){console.log("dohander")};
  function dot(){console.log("dot");}

  $(document).on("click",'#center',dohander)
  .on("click",'#center',dot)
  .on("click",dot);
</script>

經過添加處理環節,事件添加到了元素上,而且節點對應的緩存數據也添加了相應的數據。結構如下:

elemData = jQuery._data( elem );
elemData = {
  events: {
    click: {//Array[3]
      0: {
        data: undefined/{...},
        guid: 2, //處理函數的id
        handler: function dohander(){…},
        namespace: "",
        needsContext: false,
        origType: "click",
        selector: "#center",//選擇器,用來區分不同事件源
        type: "click"
      }
      1: {
        data: undefined/{...},
        guid: 3,
        handler: function dot(){…},
        namespace: "",
        needsContext: false,
        origType: "click",
        selector: "#center",
        type: "click"
      }
      2: {
        data: undefined,
        guid: 3,
        handler: function dot(){…},
        namespace: "",
        needsContext: false,
        origType: "click",
        selector: undefined,
        type: "click"
      }
      delegateCount: 2,//委託事件數量,有selector的纔是委託事件
      length: 3
    }
  }
  handle: function ( e ) {…}/*事件處理主入口*/{
    elem: document//屬於handle對象的特徵
  }
}

緩存結構特點:每一個函數添加guid;使用events對象存放響應事件列表,有一個總的事件處理入口handle等。


4.3 bind、delegate、on在上面已經介紹過了,下面介紹其他API。

one():

通過one()函數綁定的事件處理函數都是一次性的,只有首次觸發事件時會執行該事件處理函數。觸發之後,jQuery就會移除當前事件綁定。

比如$("#chua").one("click",fn);爲#chua節點綁定一次性的click事件

$(document).one("click","#chua",fn);將#chua的click事件委託給document處理。

源碼:

one: function(types, selector, data, fn) {
            return this.on(types, selector, data, fn, 1);
        },

內部也是通過調用on方法來實現。

trigger()和triggerHandler():

trigger觸發jQuery對象所匹配的每一個元素對應type類型的事件。比如$("#chua").trigger("click");

triggeHandler只觸發jQuery對象所匹配的元素中的第一個元素對應的type類型的事件,且不會觸發事件的默認行爲。

源碼:

trigger: function(type, data) {
	return this.each(function() {
		jQuery.event.trigger(type, data, this);
	});
},
triggerHandler: function(type, data) {
	var elem = this[0];
	if (elem) {
		return jQuery.event.trigger(type, data, elem, true);
	}
}

兩者通過調用jQuery.event.trigger()函數實現,稍後解析。

unbind():

unbind: function( types, fn ) {
            return this.off( types, null, fn );
        },

undelegate():

undelegate: function( selector, types, fn ) {
            // ( namespace ) or ( selector, types [, fn] )
            return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
        }

它們均調用了off函數:

off: function(types, selector, fn) {
	var handleObj, type;
	//傳入的參數是事件且綁定了處理函數
	if (types && types.preventDefault && types.handleObj) {
		// ( event )  dispatched jQuery.Event
		handleObj = types.handleObj;
		//types.delegateTarget是事件託管對象
		jQuery(types.delegateTarget).off(
			//組合jQuery識別的type
			handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType,
			handleObj.selector,
			handleObj.handler
		);
		return this;
	}
	if (typeof types === "object") {
		// ( types-object [, selector] )
		for (type in types) {
			this.off(type, selector, types[type]);
		}
		return this;
	}
	if (selector === false || typeof selector === "function") {
		// ( types [, fn] )
		fn = selector;
		selector = undefined;
	}
	if (fn === false) {
		fn = returnFalse;
	}
	return this.each(function() {
jQuery.event.remove(this, types, fn, selector);//最終都是調用jQuery.event.remove函數來解綁事件。
});},

off函數又調用了jQuery.event.remove函數,源碼分析如下:

remove: function(elem, types, handler, selector, mappedTypes) {

	var j, origCount, tmp,
		events, t, handleObj,
		special, handlers, type, namespaces, origType,
	//獲取該元素的jQuery內部數據
		elemData = data_priv.hasData(elem) && data_priv.get(elem);
//如果內部數據不存在,或者內部數據沒有events域則直接返回
	if (!elemData || !(events = elemData.events)) {
		return;
	}
//第一步:分解傳入的要刪除的事件類型types,遍歷類型,如果要刪除的事件沒有事件名,
//只有命名空間則表示刪除該命名空間下所有綁定事件
	
// Once for each type.namespace in types; type may be omitted
//分解types爲type.namespace爲單位元素的數組
	types = (types || "").match(core_rnotwhite) || [""];
	t = types.length;
	//對所有的事件類型進行遍歷
	while (t--) {
	//rtypenamespace = /^([^.]*)(?:\.(.+)|)$/;  
	//如打印[click.test,click,test]  
		tmp = rtypenamespace.exec(types[t]) || [];
		type = origType = tmp[1];
	//對付命名空間存在多個的情況,如: .aaa.bbb.ccc
	namespaces = (tmp[2] || "").split(".").sort();

		// Unbind all events (on this namespace, if provided) for the element
		//如果teype不存在,那麼移除所有的事件!也就是當前元素的events域中間的所有的數據!  
		if (!type) {
			for (type in events) {
				jQuery.event.remove(elem, type + types[t], handler, selector, true);
			}
			continue;//循環繼續
		}
		
	//第二步: 遍歷類型過程中,刪除匹配的事件,代理計數修正
		
	//獲取該類型事件的special進行特殊處理 
		special = jQuery.event.special[type] || {};
	//如果存在selector那麼就是代理對象,否則就是綁定事件到elem上面!  
	type = (selector ? special.delegateType : special.bindType) || type;
	//獲取回調函數集合  
	handlers = events[type] || [];
	//創建該命名空間下的一個正則表達式,例如:重新組合爲:xx.aaa.bbb.ccc  
		tmp = tmp[2] && new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)");

	// Remove matching events
		origCount = j = handlers.length;
		while (j--) {
			handleObj = handlers[j];//獲取該類型事件的所有的handleObj事件 
			//判斷該對象的origType,如果和handleObj一樣表示該類事件全部要移除!
			//但是必須移除的對象是origType,guid,namespace,selector都相同纔可以!
			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);//刪除handlers[j];

				if (handleObj.selector) {
					//如果是selector存在表示代理對象,那麼把delegateCount遞減!  
					handlers.delegateCount--;
				}
				if (special.remove) {
					//如果special有remove方法,那麼直接調用special的remove方法!
					special.remove.call(elem, handleObj);
				}
			}
		}
		
		//第三步:如果節點上指定類型的事件處理器已經爲空,則將events上的該類型的事件處理對象移除

		//例如 var js_obj = document.createElement("div"); js_obj.onclick = function(){ …}
		/*上面的js_obj是一個DOM元素的引用,DOM元素它長期在網頁當中,不會消失,
		而這個DOM元素的一屬性onclick,又是內部的函數引用(閉包),
		而這個匿名函數又和js_obj之間有隱藏的關聯(作用域鏈)所以形成了一個,循環引用*/
		
		//如果當前事件的回調函數集合已經爲空,
		if (origCount && !handlers.length) {
			//同時該speical沒有tearDown或者tearDown是false,那麼用removeEvent方法!
			if (!special.teardown || special.teardown.call(elem, namespaces, elemData.handle) === false) {
				jQuery.removeEvent(elem, type, elemData.handle);
			}

			delete events[type];//移除當前回調函數集合!  
		}
	}

	// Remove the expando if it's no longer used
	//如果events對象已經是空了,那麼直接連handle也移除,因爲events不存在那麼handle已經沒有存在的意義了!  
	//所以移除handle同時連events域也同時移除! 
	if (jQuery.isEmptyObject(events)) {
		delete elemData.handle;
		data_priv.remove(elem, "events");
	}
},

4.4  jQuery事件流程


那麼JQuery爲了更好的對事件的支持內部又做了哪些額外的優化操作?

兼容性問題處理:

瀏覽器的事件兼容性是一個令人頭疼的問題。IE的event在是在全局的window下, 而mozilla的event是事件源參數傳入到回調函數中。還有很多的事件處理方式也一樣

JQuery提供了一個 event的兼容類方案

jQuery.event.fix 對遊覽器的差異性進行包裝處理

例如:

  1. 事件對象的獲取兼容,IE的event在是在全局的window,標準的是event是事件源參數傳入到回調函數中
  2. 目標對象的獲取兼容,IE中採用srcElement,標準是target
  3. relatedTarget只是對於mouseout、mouseover有用。在IE中分成了to和from兩個Target變量,在mozilla中 沒有分開。爲了保證兼容,採用relatedTarget統一起來
  4. event的座標位置兼容
  5. 等等

事件的存儲優化:

jQuery並沒有將事件處理函數直接綁定到DOM元素上,而是通過.data.data存儲在緩存.cahce上,這裏就是之前分析的貫穿整個體系的緩存系統了

聲明綁定的時候:

  • 首先爲DOM元素分配一個唯一ID,綁定的事件存儲在.cahce[ID][.cahce[唯一ID][.expand ][ 'events' ]上,而events是個鍵-值映射對象,鍵就是事件類型,對應的值就是由事件處理函數組成的數組,最後在DOM元素上綁定(addEventListener/ attachEvent)一個事件處理函數eventHandle,這個過程由 jQuery.event.add 實現。

執行綁定的時候:

  • 當事件觸發時eventHandle被執行,eventHandle再去$.cache中尋找曾經綁定的事件處理函數並執行,這個過程由 jQuery.event. trigger 和 jQuery.event.handle實現。
  • 事件的銷燬則由jQuery.event.remove 實現,remove對緩存$.cahce中存儲的事件數組進行銷燬,當緩存中的事件全部銷燬時,調用removeEventListener/ detachEvent銷燬綁定在DOM元素上的事件處理函數eventHandle。

事件處理器:

jQuery.event.handlers

針對事件委託和原生事件(例如"click")綁定 區分對待

事件委託從隊列頭部推入,而普通事件綁定從尾部推入,通過記錄delegateCount來劃分,委託(delegate)綁定和普通綁定。

 

其餘一些兼容事件的Hooks

fixHooks,keyHooks,mouseHooks

 


總的來說對於JQuery的事件綁定

在綁定的時候做了包裝處理

在執行的時候有過濾器處理。

下章再看具體流程分解。

發佈了41 篇原創文章 · 獲贊 15 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章