iT邦幫忙

DAY 18
2

Javascript面面觀系列 第 18

Javascript面面觀:網頁篇《jQuery inside - events》

jQuery另一個重要的組成部分就是事件處理。熟悉網頁Javascript開發的人應該都知道,IE的事件機制與Firefox及其他瀏覽器的事件機制不太一樣,jQuery利用DOM3 Event規格把事件包裝成一致的API來使用。
一開始就提到,jQuery把瀏覽器事件包裝成類似DOM3 Event規格的物件,這樣就可以為事件提供一致的介面與使用方法。其實在抓到dom node之後,jQuery是利用一些事件函數讓你把事件處理函數掛到dom node上去。這些事件處理函數的第一個參數,就是事件發生時傳給事件處理函數的事件物件。這個事件處理函數的寫法,就是DOM3 Event制定的。而這個事件物件,也盡量符合規格。

jQuery內部使用幾個函數及物件來建構他的核心事件機制:(2433~2797行)
* jQuery.event.add(elem, types, handler, data)
* jQuery.event.remove(elem, types, handler)
* jQuery.event.trigger( event, data, elem, bubbling )
* jQuery.event.handle(event)
* jQuery.event.fix(event)
* jQuery.event.proxy( fn, proxy )
* jQuery.event.special
* jQuery.event.specialAll

關於special及specialAll是用來提供自訂事件機制的,可以參考:http://docs.jquery.com/Events/jQuery.Event最底下兩篇部落格文章的說明,不過要注意,第二篇提到的add跟remove這兩個hook是要到1.3.3版才提供。

傳遞給事件處理函數的物件,則是使用jQuery.Event()函數來當作constructor。(可以把他當作factory method,也可以直接new他,相關的程式集中在2799~2861行)

實際在使用時的外部API,像是click/bind/toggle/hover/ready等等,則是在2894~3101行掛上去。

接下來,就追蹤幾個事件實際上如何被處理的,先來看看click事件...在3093~3101行有一段程式:

 jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
 	"mousedown,mouseup,mousemove,mouseover,mouseout,mouseenter,mouseleave," +
 	"change,select,submit,keydown,keypress,keyup,error").split(","), function(i, name){
 
 	// Handle event binding
 	jQuery.fn[name] = function(fn){
 		return fn ? this.bind(name, fn) : this.trigger(name);
 	};
 });

(為何用這個方式寫?不過這是個簡潔的做法)先把要加進系統的事件字串轉成陣列,然後用jQuery.each傳進來的函數處理,每個陣列元素處理一次,陣列索引及內容用i及name變數傳進函數。在函數中,則為jQuery.fn增加一個操作方法,key就是傳進來的name,而這個函數如果有收到傳進來的參數fn,就用jQuery.fn.bind(name, fn)處理,否則就呼叫this.trigger(name)...恩,總之,在碰到'click'字串時,就會這樣加入click()方法:

jQuery.fn['click'] = function(fn) {
	return fn? this.bind('click', fn) : this.trigger('click');
};

照我們使用的邏輯看起來$('#id').click(function(){})會註冊一個click事件,$('#id').click()則會觸發這個事件。所以this.bind('click', fn)會註冊click事件處理函數fn給selector指定的node,而this.trigger('click')則會觸發這個node已註冊的事件,執行事件處理函數。

所以接下來要去看看jQuery.fn.bind以及jQuery.fn.trigger在做些什麼。這兩個函數在稍微上面的地方:(2894~2921行)

 jQuery.fn.extend({
 	bind: function( type, data, fn ) {
 		return type == "unload" ? this.one(type, data, fn) : this.each(function(){
 			jQuery.event.add( this, type, fn || data, fn && data );
 		});
 	},
 ......
 	trigger: function( type, data ) {
 		return this.each(function(){
 			jQuery.event.trigger( type, data, this );
 		});
 	},
 ......
 });

先來看看bind做什麼好了。嗯嗯,所以如果剛剛傳進來的是'unload',就會呼叫this.one(type, data, fn),如果不是(例如'click'),那就每個selector找到的node呼叫一次函數,這個函數裡面會呼叫jQuery.event.add( this, type, fn || data, fn && data )。剛剛是呼叫this.bind('click',fn),所以type='click'而data=fn。所以接下來要看一下jQuery.event.add(),他定義在更前面(2437~2516行)...程式太長,沒辦法在這裡列出,不過他頻繁使用一個函數叫做jQuery.data(),所以先找一下這是什麼吧。(1269~1298行)

 var expando = "jQuery" + now(), uuid = 0, windowData = {};
 
 jQuery.extend({
 	cache: {},
 
 	data: function( elem, name, data ) {
 		elem = elem == window ?
 			windowData :
 			elem;
 
 		var id = elem[ expando ];
 
 		// Compute a unique ID for the element
 		if ( !id )
 			id = elem[ expando ] = ++uuid;
 
 		// Only generate the data cache if we're
 		// trying to access or manipulate it
 		if ( name && !jQuery.cache[ id ] )
 			jQuery.cache[ id ] = {};
 
 		// Prevent overriding the named cache with undefined values
 		if ( data !== undefined )
 			jQuery.cache[ id ][ name ] = data;
 
 		// Return the named cache data, or the ID for the element
 		return name ?
 			jQuery.cache[ id ][ name ] :
 			id;
 	},

簡單地說,jQuery內部有一個計數器變數uuid,只要使用jQuery.data存東西時,只要這個dom node沒有使用jQuery.data存過資料,這個計數器便會加一當作為一的索引,然後把這個索引存在dom node裡(elem[expando],expando是每次載入jquery時都會重新產生的變數),然後用這個uuid把資料存在jQuery[uuid][name]裡面。

回頭去看jQuery.event.add(),可以發現事件處理函數最後會產生一個唯一的guid然後存進jQuery.cache[uuid]['events']['click'][guid],並且依照event type判斷是否為special event,如果是的話則呼叫jQuery.event.special中存放此event type的setup函數來起始,否則就是一般事件,透過IE的attachEvent或其他遵循DOM3 Event標準的addEventListener方法把一個統一的函數當作事件處理函數掛上,這個事件處理函數會呼叫jQuery.event.handle.apply(node, arguments)。

呼叫trigger()時,會依照傳入的name也就是定義好的event type來判斷是否為special event,如果是的話就依照自訂事件的方法處理,否則就會執行預設的事件處理函數,這個事件處理函數會如上文說的呼叫jQuery.event.handle(node, ...),而handle就會從jQuery.cache[node.expando]['events']['click']把存入的所有事件處理函數調出來執行,同時把jQuery.Event配合事件發生的狀態做設定後傳給這個函數當作參數。

其實程式比這個還複雜,我已經把處理過程稍微簡化以方便理解了。有空的話,依照這樣的順序看jQuery的原始碼就可以知道他是怎麼運作的。

至於自訂事件怎麼做,可以參考兩篇文章(我上文也有提到):
* http://brandonaaron.net/blog/2009/03/26/special-events
* http://brandonaaron.net/blog/2009/06/4/jquery-edge-new-special-event-hooks這篇講的是1.3.3將支援的自訂事件處理方法,使用目前的穩定版本1.3.2會有問題。

經過這樣的包裝,最後,所有的事件都或變成jQuery.fn.click()等函數,可以直接在程式裡面用一致的方法操作。


上一篇
Javascript面面觀:網頁篇《jQuery inside - selector》
下一篇
Javascript面面觀:網頁篇《jQuery inside - plugin》
系列文
Javascript面面觀30

尚未有邦友留言

立即登入留言