jQuery的事件綁定有幾個比較優秀的特點:
1. 可以綁定不限數量的處理函數
2. 事件可以委托到祖先節點,不必一定要綁到對應的節點,這樣后添加的節點也照樣能被處理。
3. 鏈式操作
下面主要分析事件的委托設計。事件源我們成為委托節點,委托節點委托他的祖先節點替他執行事件處理,這個祖先節點被成為被委托節點。
DOM的原生事件將處理綁定在相應的節點上,相應節點觸發事件才能執行處理。將事件處理委托給祖先節點,這個事件處理是附加到祖先節點的。那么需要做到的是,原節點觸發了事件,想要執行已經附加到祖先節點的事件處理那么就需要保證祖先節點也需要觸發相同的事件,並且知道實際上是觸發了原節點。如何能做到這一點?事件冒泡機制提供了這種可能。
a. 深入理解事件冒泡
先放一張自己畫的事件模型
DOM的事件流程分為三個階段:
第一階段:事件捕獲。所謂的事件捕獲就是從最大范圍(document)開始,一級一級往下找,知道找到最精確的事件源(觸發事件的節點,模型中事件源是#small節點)為止的過程。舉個簡單的例子,要找到A學校B年級C班D同學;那我先要找到A學校,然后找到B年級,然后在找到C班,最后我才能准確的找到D同學。
第二階段:事件觸發。找到了事件源,接下來就應當執行綁定的相應的事件(如果有的話)。舉個例:告訴D同學到12點了(這個就好比是事件類型),然后D同學就知道到是吃中午飯的時間了,然后就去吃中午飯(執行相應的事件)。
第三階段:事件冒泡。事件源執行完事件處理后。這個類型的事件會向祖先節點傳遞,直到document(包括document)(當然是在事件冒泡沒有被阻止的前提下,后續的分析都是基於這個前提下)。也就是說事件源的每一個祖先節點都會觸發同類事件。還是上面的例子,D同學去吃飯了,然后C班知道到12點了(事件類型),然后全班放學(執行事件)...最后A學校也收到了12點的消息(事件類型),然后學校下課鈴響了(執行相應事件)。當然這只是一個例子,里面的內容不必當真。
事件冒泡這么好的特性jQuery當然要好好利用。事件源觸發了某個事件,其祖先節點必然也會觸發該類事件,而且祖先還知道冒泡到我這里的事件的事件源是那個后代節點(event.target)。這便給了事件委托提供了必要條件。
想象一下,把事件源a觸發click事件的處理委托給其祖先節點b。當a節點觸發click,事件冒泡到b的時候,b節點也觸發click事件,然后b一看事件列表中有一個委托事件,這個委托事件保存了委托節點的選擇器,這個選擇器所匹配節點就是事件源a,那么b馬上執行這個委托事件(當然jQuery做的更為復雜一些,委托節點只要是a到b之間的節點且事件類型也和觸發的事件類型相同就會執行其委托處理)。
舉個實例
<style> #big{ width: 400px; height: 400px; background-color: #00f; } #middle{ width: 200px; height: 200px; background-color: #000; } #small{ width: 100px; height: 100px; background-color: #f00; } </style> <div id="big"><div id="middle"><div id="small"></div></div></div>
<script> document.getElementById('big').onclick = function(){console.log("big clicked!")} document.getElementById('middle').onclick = function(){console.log("middle clicked!")} document.getElementById('small').onclick = function(){console.log("small clicked!")} </script>
點擊最小的那個紅塊(#small)。執行結果如下
b. jQuery事件委托處理流程
上一章分析jQuery.event.add的時候已經分析了事件綁定,再把綁定的部分源碼抽出來
if ( !(eventHandle = elemData.handle) ) { eventHandle = elemData.handle = function( e ) { //當一個事件被調用后頁面已經卸載,則放棄jQuery.event.trigger()的第二個事件, return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : undefined; }; //將elem作為handle函數的一個特征防止ie非本地事件引起的內存泄露 eventHandle.elem = elem; }
...
//非自定義事件,如果special事件處理器返回false,則只能使用addEventListener/attachEvent if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { //給元素綁定全局事件 if ( elem.addEventListener ) { elem.addEventListener( type, eventHandle, false ); } else if ( elem.attachEvent ) { elem.attachEvent( "on" + type, eventHandle ); } }
綁定到elem上的事件處理是eventHandle,最終執行eventHandle的時候實際執行的是事件調度jQuery.event.dispatch。事件調度的的流程實際上就是處理委托事件的流程,因為本節點的響應處理最終會被附加到委托處理列表后面。
事件調度流程為
1. 從本地事件對象event構造一個可寫的jQuery.Event對象。並用這個對象替換掉傳參中的本地事件對象
//從本地事件對象構造一個可寫的jQuery.Event event = jQuery.event.fix( event );
...
//使用修正過得jQuery.Event而不是(只讀的)本地事件
args[0] = event;
event.delegateTarget = this;
本地事件event的結構如下
使用本地事件構造的新事件對象jQuery.Event結構如下
其中originalEvent屬性的值便是本地事件對象。構造的這個事件對象有很多屬性都是直接從本地事件對象中抽出來的。
2. 獲取當前節點緩存中對應事件類型的事件處理列表
handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [],
事件處理列表的的順序是委托事件處理在前面,最后才是直接綁定到當前節點的事件處理。
3. 用當前節點替換jQuery.event.handlers的調用者並執行之,獲取到符合要求的委托處理函數隊列(這個隊列最后會加上綁定到節點本身的處理事件)
//獲取指定的事件處理隊列,主要使用event.target事件源節點不斷循環往上查找父節點, //看些節點和是否在handlers中的選擇器對應的節點中 handlerQueue = jQuery.event.handlers.call( this, event, handlers );
詳細分析一下jQuery.event.handlers中獲取符合要求的委托處理函數隊列。
jQuery.event.handlers先將委托事件處理取出來放在處理隊列handlerQueue中。
查找的過程是:先取出事件源cur = event.target;然后在確定有委托處理的情況下從事件源開始往他的祖先節點查詢,遍歷委托事件列表中的每一個委托事件處理所指定的響應節點(委托事件處理對象的selector所指定)是否包含查詢的節點【handleObj.needsContext ?jQuery( sel, this ).index( cur ) >= 0 :jQuery.find( sel, this, null, [ cur ] ).length】,如果包含則往事件處理隊列handlerQueue中壓入該委托處理。源碼如下
if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { //冒泡父節點,找到匹配的委托事件存入handlerQueue隊列 for ( ; cur != this; cur = cur.parentNode || this ) { if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { matches = []; for ( i = 0; i < delegateCount; i++ ) {
// 避免和Object.prototype屬性沖突(#13203)
sel = handleObj.selector + " ";
if ( matches[ sel ] === undefined ) {
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 });
}
}
}
}
最后將直接綁定到當前節點的處理也壓入執行
//添加直接綁定的事件到handlerQueue隊列中 if ( delegateCount < handlers.length ) { handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); }
4. 執行事件處理隊列handlerQueue中的處理函數
//先運行代理,他們可能是阻止冒泡的,我們可以利用這一點 i = 0; while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { event.currentTarget = matched.elem; j = 0; while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { //觸發事件的條件:1)沒有命名空間,或 // 2)有命名空間的子集或等於那些邊界事件(他們兩者都可以沒有命名空間) if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { event.handleObj = handleObj; event.data = handleObj.data; //執行 ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) .apply( matched.elem, args ); if ( ret !== undefined ) { if ( (event.result = ret) === false ) { event.preventDefault(); event.stopPropagation(); } } } } }
這里面有一個點需要注意。構造的新事件對象event本來是被委托的節點的事件對象(event.currentTarget可以作證),但是在執行被委托的處理的的時候,事件對象被轉換成了委托節點的事件對象(event.currentTarget = matched.elem;),這樣保證被委托事件處理中調用的事件對象的正確性。舉例:還是前面的那段html,只不過執行代碼變成了
function dohander(e){ alert("dohander"); }; function dot(e){ e.stopPropagation(); alert("dot"); };
$(document).on("click",'#big',dohander) .on("click",'#middle',dot) .on("click",'#small',dohander);
"#middle"節點委托document節點執行處理dot,dot中有一段代碼e.stopPropagation();當點擊"#middle"節點,事件冒泡到document。在執行dot之前如果事件沒有被轉換成被委托的節點的事件,那么這個阻止冒泡並不是阻止"#middle"節點的事件冒泡,而是阻止document節點的事件冒泡。
當然,實際上在處理被委托的事件的時候,事件已經冒泡到被委托節點document了。我們是沒法對已經冒泡過了的節點進行阻止,比如"#big"節點就已經觸發過了click事件。但是我們可以對被委托到document節點的委托事件處理列表做模擬阻止冒泡處理。點擊"#middle"節點,事件冒泡到document,執行jQuery.event.handlers得到委托事件處理列表是這樣的
//"#small"節點的委托事件處理已經被jQuery.event.handlers過濾掉了 handlerQueue = ["#middle"節點的委托事件處理dot, "#big"節點的委托事件處理dohander];
執行dot中的e.stopPropagation(),stopPropagation也是重載過的,源碼如下
stopPropagation: function() { var e = this.originalEvent; this.isPropagationStopped = returnTrue; if ( !e ) { return; } // If stopPropagation exists, run it on the original event if ( e.stopPropagation ) { e.stopPropagation(); } // Support: IE // Set the cancelBubble property of the original event to true e.cancelBubble = true; }
其中this.isPropagationStopped = returnTrue;事件被標記為阻止冒泡。執行完dot后進入下一個執行handerQueue的下一個委托處理“"#big"節點的委托事件處理dohander”,這里有一個判斷
while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() )
事件event.isPropagationStopped() = returnTrue() = (function returnTrue() {return true;})();阻止進入while執行下一個委托處理。OK,后面的委托事件全部被阻止處理了,所以最終點擊“#middle”節點只執行了dot函數。
模擬阻止冒泡過程結束。
下面用一個更加完整的實例來說明,js代碼
function dohander(e){ alert("dohander"); }; function dot(e){ e.stopPropagation(); alert("dot"); }; function doBigOnly(e){ alert("big"); } $(document).on("click",'#big',dohander) .on("click",'#middle',dot) .on("click",'#small',dohander); $('#big').click(doBigOnly);
點擊“#middle”節點,執行結果:先彈出alert("big"),然后彈出alert("dot")
流程分析:
點擊"#middle","#middle"上沒有綁定事件。
事件冒泡到"#big","#big"綁定了處理doBigOnly直行之顯示alert("big")。
事件冒泡到body,
然后冒泡到html,
最后冒泡到document。document上有綁定事件,根據事件源過濾掉"#small"的委托處理。剩下兩個委托("#middle"委托處理和"#big"委托處理)需要處理。先處理"#middle"委托處理dot,直行之顯示alert("dot"),阻止事件冒泡。后續委托被阻止。
完畢。