最原始的事件注冊
addEventListener方法大家應該都很熟悉,它是Html元素注冊事件最原始的方法。先看下addEventListener方法簽名:
element.addEventListener(event, function, useCapture)
event:事件名,例如“click”,這里要提醒的一點是不要加前綴“on”;
function:事件觸發時執行的函數;
userCapture:默認為false,表示event事件在冒泡階段觸發。如果設置為true,則事件將會在捕獲階段觸發。如果不清楚什么是捕獲和冒泡,請自覺了解事件的冒泡機制(友情鏈接:勤能補挫-簡單But易錯的JS&CSS問題總結)。
雖然addEventListener包含了三個參數,但一般我們都只使用了前兩個參數,下面的代碼只使用了兩個參數:
document.getElementById("myBtn").addEventListener("click", function() {
alert(“我是在冒泡階段觸發的哦!”);
});
上面代碼注冊的函數會在冒泡階段觸發,如果想在捕獲階段觸發,直接把第三個參數傳遞進去就ok了。在實現DOM元素拖拽功能時,會使用到捕獲方式。
另外,IE8以及之前的版本不支持事件按捕獲形式傳播,並且注冊方法也沒有addEventListener函數,IE為事件注冊提供了attachEvent方法。和addEventListener相似,也包含有event和function參數,但不包含第三個參數。
jQuery事件注冊
jQuery的事件函數通過jQuery.fn.extend附加到jQuery對象,jQuery.fn.extend包含了jQuery的所有事件注冊函數。那么jQuery到底提供了哪些事件函數?這里把這些函數分層了三類:
(1)和事件同名的函數:jQuery幾乎提供了所有DOM元素事件的同名函數,像我們經常使用的click、focus、scroll等函數。使用也很簡單,例如我們要給div元素綁定click事件,可以直接寫成$(“div”).click(function(){})。DOM元素的事件有很多,jQuery為每個事件都添加了同名的注冊函數嗎?看源碼!
//循環遍歷所有的dom元素事件名數組
jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
"change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) {
//把dom元素所有事件通過fn[事件名]的方式添加到jquery對象
// Handle event binding
jQuery.fn[ name ] = function( data, fn ) {
//如果參數長度大於0,則調用on方法委托函數到name事件;如果參數長為0,則觸發事件執行
return arguments.length > 0 ?
this.on( name, null, data, fn ) :
this.trigger( name );
};
});
首先看到的是一串包含了所有DOM元素事件的字符串,通過空格把字符串分隔成數組。如果傳遞的參數長度大於0,則調用jQuery對象的on方法注冊事件。如果參數長度為0,則直接調用trigger方法觸發事件。例如(“div”).click(function())將會調用on方法注冊事件,而(“div”).click()則調用trigger方法,立即觸發click事件。
上面的代碼有幾點需要作下解釋:
jQuery.fn中的函數包含的上下文this是指向jQuery實體,例如$(“div”)實體。
jQuery.fn[name] = function(){}等效於jQuery.fn.name = function(){},例如jQuery.fn[“click”] = function(){}等效於jQuery.fn.click = function(){}。
This.on和this.trigger方法這里暫不忙解釋。
(2)綁定和委托函數:bind/unbind和delegate/undelegate方法通過jQuery.fn.extend附加到jQuery對象上。代碼很簡單:
jQuery.fn.extend({
//事件綁定
bind: function( types, data, fn ) {
return this.on( types, null, data, fn );
},
//事件解綁
unbind: function( types, fn ) {
return this.off( types, null, fn );
},
//事件委托
delegate: function( selector, types, data, fn ) {
return this.on( types, selector, data, fn );
},
//委托解綁
undelegate: function( selector, types, fn ) {
return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
}
});
bind和delegate都是直接調用jQuery對象的on函數,唯一區別是傳遞的參數不同,bind的第二個參數為null,而委托的第二個參數是一個selector。別小看這個區別,使用jQuery綁定事件常出的問題部分原因就是沒搞清楚這兩個參數的區別。
(3)底層注冊函數:前面介紹的和事件同名的函數、綁定和委托函數最終都是調用了jQuery對象的on函數,我們在編程的時候也可以直接使用on函數。on函數代碼比較復雜,我們先看看外殼:
jQuery.fn.extend({
//比較底層的事件委托函數,其他函數都是調用這個來和元素建立綁定或者委托
on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
return this.each( function() {
jQuery.event.add( this, types, fn, data, selector );
});
},
//一次性事件綁定
one: function( types, selector, data, fn ) {
return this.on( types, selector, data, fn, 1 );
},
//比較底層的事件解綁,其他解綁函數都是調用該函數執行解綁
off: function( types, selector, fn ) {
return this.each(function() {
jQuery.event.remove( this, types, fn, selector );
});
},
//觸發事件
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 );
}
}
});
為什么說是底層的函數?因為前面的所有綁定最終都是調用on函數,所有的解綁最終調用off函數。這里還包含了trigger和triggerHandler函數,前一個是觸發元素的所有type事件行為,而triggerHandler只觸發綁定的函數而不觸發行為。例如focus事件,triggerHandler只會觸發綁定給元素的focus處理函數,而不會真的讓元素獲得焦點。但trigger函數會讓元素獲取焦點。
匯總一下,jQuery提供的事件處理函數不外乎也就下面這些。
委托還是綁定?
這里為什么提出了委托和綁定?事出有因,我們慢慢來分析。之前介紹了幾類事件綁定,先分下類便於后面的分析。以什么分類?就以調用on函數的第二個參數為不為null。
(1)為null的一類on(types, null, data, fn)事件
bind、blur、focus、focusin、focusout、load、resize、scroll、unload、click、dblclick、mousedown、mouseup、mousemove、mouseover、mouseout、mouseenter、mouseleave、 change、select、submit、keydown、keypress、keyup error、contextmenu。
(2)不為null的一類on(types, selector, data, fn)事件
delegate、on
接下來我們舉一個場景:給div容器(class為parent)列表中的每一項(class為child)添加click事件,並且列表的項可動態添加。
<div class="parent"> <div class="child">第1個兒子</div> <div class="child">第2個兒子</div> <div class="child">第3個兒子</div> </div> <button id="btn">生兒子</button> <script type="text/javascript"> var i = 4; (".parent.child").click(function(){alert("我是你兒子"}); //(".parent.child").click(function(){alert("我是你兒子"}); //(".parent .child").bind("click", function(){ // alert("我是你兒子"); // }) ("#btn").click(function(){ $(".parent").append("<div class='child'>第" + (i++) + "個兒子</div>"); }); </script>
頁面加載后點擊前三個兒子都會提示“我是你兒子”,現在我點擊btn按鈕,添加第四個兒子,然后再點擊新增項看看。發現沒有再彈出提示信息。上面代碼注冊事件使用的是click或者bind函數,效果都是一樣:動態添加的子項沒有觸發事件了。其實,“為null的一類”事件效果都是這樣。現在我們再把事件綁定改成delegate或者on函數:
//(".parent").on("click", ".child", function(){
// alert("我是你兒子");
// });
$(".parent").delegate(".child", "click", function(){
alert("我是你兒子");
});
測試結果發現,不管是on或者delegate,我們后面動態添加的子項都能觸發事件。
通過上面的場景不難看出,click和bind函數只支持靜態綁定,只能綁定給已經有的節點,后期動態生成的節點不支持。這樣的行為我們可稱為“綁定”。而通過delegate或者on方法通過傳遞一個selector,把通過selector篩選的元素的事件全權“委托”給父容器。所以事件其實是綁定在父容器上,只是在處理事件時jQuery內部做了委托處理。
那么,到底是委托好還是綁定好?個人建議如果篩選的元素比較少,可以使用click或者bind,比較簡單並且代碼也容易理解。但如果篩選出的元素可能包含成百上千,那么肯定使用delegate或者on,這樣性能比bind高多了。delegate、on事件只會綁定給父容器,即使1000個節點,還是只綁定一次。而bind的話就得乖乖的綁定1000次。
不管是委托還是綁定,都是通過on注冊。所以搞清楚on函數的實現也就搞清楚了jQuery的事件機制。
jQuery源代碼分析
jQuery.fn.on函數
既然綁定和委托最終都是調用on函數,那么只要把on方法代碼流程了解清楚,整個事件綁定機制也了解的差不多。On函數代碼其實比較簡單,包含參數處理和事件添加兩個部分。函數包含了5個參數:
on: function( types, selector, data, fn, /*INTERNAL*/ one )
但是我們經常使用on函數並沒有傳遞這么多參數,而是像這樣:
(“a”).on(“click”,function());(“a”).on(“click”,function());(“a”).on(“click”, “p”, function(){}); (“a”).on(“click,mouseover,focus”,function());
(“a”).on(“click,mouseover,focus”,function());
(“”).on(“click”, {id: 1, name: “test”}, function{});
on函數大部分代碼都是處理傳入的參數,最后三行代碼使用each遍歷jQuery對象中的元素並調用jQuery.event.add方法。源代碼如下:
<DIV class=cnblogs_code style="BORDER-TOP: #cccccc 1px solid; BORDER-RIGHT: #cccccc 1px solid; BORDER-BOTTOM: #cccccc 1px solid; PADDING-BOTTOM: 5px; PADDING-TOP: 5px; PADDING-LEFT: 5px; BORDER-LEFT: #cccccc 1px solid; PADDING-RIGHT: 5px; BACKGROUND-COLOR: #f5f5f5"><PRE><SPAN style="COLOR: #000000">jQuery.fn.extend({ //比較底層的事件委托函數,其他函數都是調用這個來和元素建立綁定或者委托 on: function( types, selector, data, fn, /*INTERNAL*/ one ) { var origFn, type; //參數為types/handlers,("click", function) if ( typeof types === "object" ) { // ( types-Object, selector, data )。例如({'click': function1,'focus': function2}, selector, data) if ( typeof selector !== "string" ) { // ( types-Object, data )。例如({'click': function1,'focus': function2}, data) data = data || selector; selector = undefined; } //遍歷{'click': function1,'focus': function2} for ( type in types ) { //每個type再單獨調用on注冊一次 this.on( type, selector, data, types[ type ], one ); } return this; } //只有兩個參數,{types,fn} if ( data == null && fn == null ) { // ( types, fn ) fn = selector; data = selector = undefined; } //fn == null && data != null,只有三個參數的情況 else if ( fn == null ) { if ( typeof selector === "string" ) { // ( types, selector, fn ),例如:("click", "a,p", function(){}) fn = data; data = undefined; } else { // ( types, data, fn ), 例如:("click", {id: 1, name: "test"}, function(e){}) fn = data; data = selector; selector = undefined; } } if ( fn === false ) { //如果fn等於false,重新賦給fn一個return false的函數。 fn = returnFalse; } else if ( !fn ) { //如果fn未定義或者為null,不做任何操作,直接返回鏈式對象this return this; } if ( one === 1 ) { //事件只執行一次 origFn = fn; fn = function( event ) { //重寫fn函數,在執行fn函數一次后,注銷事件 // Can use an empty set, since event contains the info jQuery().off( event ); return origFn.apply( this, arguments ); }; // Use same guid so caller can remove using origFn fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); //賦值fn.guid等於原始函數origFn.guid } //jQuery對象包含的元素是一個集合,所以需要遍歷每個元素執行event.add return this.each( function() { //event.add做了什么操作? jQuery.event.add( this, types, fn, data, selector ); }); } }
jQuery.event對象
jQuery.fn.on函數最后三行代碼調用了jQuery.event.add函數,add是jQuery.event的一個函數。在了解add之前先看看jQuery.event,jQuery.event究竟包含哪些東西:
jQuery.event = { //函數,為元素添加事件 add: function( elem, types, handler, data, selector ) {}, //函數,為元素刪除事件 remove: function( elem, types, handler, selector, mappedTypes ) {}, //函數,觸發元素事件 trigger: function( event, data, elem, onlyHandlers ) {}, //函數,執行元素事件 dispatch: function( event ) {}, //函數,事件隊列 handlers: function( event, handlers ) {}, //屬性,KeyEvent和MouseEvent事件屬性 props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), //函數,擴展event。添加一些附加屬性,像target、type、origainEvent等屬性 fix: function( event ) {}, //對象,特殊事件 special: {}, //函數,模擬事件行為,例如focus、unfocus行為 simulate: function( type, elem, event, bubble ) {} }
現在我們想要搞清楚的是jQuery怎樣添加事件,以及如何執行事件。要了解清楚這些問題,就必須得搞清楚代碼中的add、dispatch、handlers三個函數。
為了容易理解這些函數的關系,下面是一個函數執行順序的流程圖:
jQuery.event.add函數
事件是建立在DOM元素之上,DOM元素和事件要建立關系,最原始的方法是在DOM元素上綁定事件。jQuery為了不破壞DOM樹結構,通過緩存的方式保存事件。jQuery內部有一個叫做Data的緩存對象,通過key/value這種方式緩存數據。細心的同學在使用jQuery時會發現DOM元素多了一個以jQuery開頭的屬性,例如jQuery20303812802915245450.4513941336609537:3。這個屬性正是jQuery緩存的key值。
Add函數中的elemData就是一個類型為Data的緩存對象,在調用get時需要把元素作為參數傳遞進去, 查找元素的屬性以jQuery開始的元素句柄。例如elem[‘jQuery203038128.l..537’]這種形式。elemData需要關注另外兩個屬性:handle和events。
handler就是一個調用了dispatch的匿名函數,events是一個數組,每一項是一個handleObj對象,包含type、origType、data、handler、guid、selector等屬性。如果傳遞的types為”click focus mouseenter”,那么events數組就包含了三個handleObj對象。
另外還得調用addEventListener給委托元素注冊事件,不然事件觸發不了。
總得來說,add函數干了幾件事:
如果沒有為委托元素elem建立緩存,在調用get時創建緩存;
賦予elemData.handle一個匿名函數,調用event.dispatch函數。
往elemData.events數組添加不同事件類型的事件對象handleObj。
給elem綁定一個types類型的事件,觸發時調用elemData.handle。
add: function( elem, types, handler, data, selector ) { var handleObjIn, eventHandle, tmp, events, t, handleObj, special, handlers, type, namespaces, origType, elemData = data_priv.get( elem ); //存儲事件句柄對象,elem元素的句柄對象 if ( !handler.guid ) { handler.guid = jQuery.guid++; //創建編號,為每一個事件句柄給一個標示 } if ( !(events = elemData.events) ) { events = elemData.events = {}; //events是jQuery內部維護的事件列隊 } if ( !(eventHandle = elemData.handle) ) { //handle是實際綁定到elem中的事件處理函數 eventHandle = elemData.handle = function( e ) { jQuery.event.dispatch.apply( eventHandle.elem, arguments ); }; eventHandle.elem = elem; //事件可能是通過空格鍵分隔的字符串,所以將其變成字符串數組 types = ( types || "" ).match( core_rnotwhite ) || [""]; t = types.length; while ( t-- ) { // 這里把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 ); // 初始化事件處理列隊,如果是第一次使用,將執行語句 if ( !(handlers = events[ type ]) ) { handlers = events[ type ] = []; handlers.delegateCount = 0; if ( elem.addEventListener ) { elem.addEventListener( type, eventHandle, false ); } } // 將事件處理對象推入處理列表,姑且定義為事件處理對象包 if ( selector ) { handlers.splice( handlers.delegateCount++, 0, handleObj ); } else { handlers.push( handleObj ); } // 表示事件曾經使用過,用於事件優化 jQuery.event.global[ type ] = true; } // 設置為null避免IE中循環引用導致的內存泄露 elem = null; }
jQuery.event.dispatch函數
委托元素觸發事件時會調用dispatch函數,dispatch函數需要做的就是執行我們添加的handler函數。
jQuery事件中的event和原生event是有區別的,做了擴展。所以代碼中重新生成了一個可寫的event:jQuery.event.fix(event)。包含的屬性:
delegateTarget、currentTarget、handleObj、data、preventDefault、stopPropagation。
由於我們添加的事件函數之前保存到了緩存中,所以調用data_priv.get取出緩存。
代碼生成了一個handlerQueue隊列,這里先不忙介紹jQuery.event.handlers函數。handlerQueue是一個數組,每一項是一個格式為{ elem: cur, handlers: matches }的對象。cur是DOM元素,handlers是處理函數數組。
兩個while循環:
第一個循環遍歷handlerQueue,item為{ elem: cur, handlers: matches }。
第二個循環遍歷handlers,分別執行每一個handler。
event做了封裝,我們可以在事件函數中通過event.data獲取額外的信息。
dispatch函數有判斷處理函數的返回結果,如果返回結果等於false,阻止冒泡。調用preventDefault、stopPropagation終止后續事件的繼續傳遞。
dispatch: function( event ) { //把event生成一個可寫的對象 event = jQuery.event.fix( event ); var handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || []; event.delegateTarget = this; handlerQueue = jQuery.event.handlers.call( this, event, handlers ); i = 0; while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { event.currentTarget = matched.elem; j = 0; while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { event.handleObj = handleObj; event.data = handleObj.data; ret = handleObj.handler.apply( matched.elem, args ); if ( ret !== undefined ) { if ( (event.result = ret) === false ) { event.preventDefault(); event.stopPropagation(); } } } } } return event.result; }
jQuery.event.handler函數
dispatch函數有調用handler函數生成一個handler隊列,其實整個事件流程中最能體現委托的地方就是handler函數。
這里有兩個端點,cur = event.target(事件觸發元素)和this(事件委托元素)。jQuery從cur通過parentNode 一層層往上遍歷,通過selector匹配當前元素。
每一個cur元素都會遍歷一次handlers。handlers的項是一個handleObj對象,包含selector屬性。通過jQuery( sel, this ).index( cur )判斷當前元素是否匹配,匹配成功就加到matches數組。
handlers遍歷完后,如果matches數組有值,就把當前元素cur和matches作為一個對象附加到handlerQueue中。
一個委托元素可能包含委托和普通事件(直接綁定的事件),目前我們只根據delegateCount遍歷了委托事件,所以最后還得通過handlers.slice( delegateCount )把后面的普通事件添加到隊列中。
什么是委托事件和普通事件?
(“div”).on(“click”,“a,p”,function)這種形式添加的function是div的委托事件;而像(“div”).on(“click”, function)形式添加的事件就是div元素的一個普通事件。handlers數組中delegateCount之前的都是委托事件,之后的是普通事件。
handlers: function( event, handlers ) { var handlerQueue = [], delegateCount = handlers.delegateCount, cur = event.target; //向上遍歷DOM元素 for ( ; cur !== this; cur = cur.parentNode || this ) { if ( cur.disabled !== true || event.type !== "click" ) { matches = []; for ( i = 0; i < delegateCount; i++ ) { handleObj = handlers[ i ]; //獲取handler的selector sel = handleObj.selector + " "; if ( matches[ sel ] === undefined ) { matches[ sel ] = handleObj.needsContext ? //查看通過selector篩選的元素是否包含cur jQuery( sel, this ).index( cur ) >= 0 : jQuery.find( sel, this, null, [ cur ] ).length; } //如果元素匹配成功,則把handleObj添加到matches數組。 if ( matches[ sel ] ) { matches.push( handleObj ); } } //如果matches數組長度大於0,附加cur和matches到隊列中 if ( matches.length ) { handlerQueue.push({ elem: cur, handlers: matches }); } } } if ( delegateCount < handlers.length ) { //表示還有為委托事件函數,也要附加到隊列中 handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); } return handlerQueue; }
如果本篇內容對大家有幫助,請點擊頁面右下角的關注。如果覺得不好,也歡迎拍磚。你們的評價就是博主的動力!下篇內容,敬請期待!