jQuery事件系統並無將事件監聽函數直接綁定到DOM元素上,而是基於數據緩存模塊來管理監聽函數的,事件模塊代碼有點多,我把它分爲了三個部分:分底層方法、實例方法和便捷方法、ready事件來說,好理解一點。html
jQuery的事件分爲普通事件和代理事件:node
事件系統模塊的底層方法以下:jquery
經常使用的就是以上三個吧,其它還有$.event.global(記錄綁定過的事件)、$.event.removeEvent(用於刪除事件)等api
先舉栗子前先先簡單說一下jQuery裏的事件的分類,jQuery的事件分爲普通事件和代理事件:數組
這樣說可能理解不了,舉個栗子,咱們定義兩個DOM元素先瀏覽器
這裏咱們定義了一個div,內部含有一個子節點button,渲染以下:緩存
舉個栗子:app
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="http://libs.baidu.com/jquery/1.7.1/jquery.min.js"></script> <style>div{width: 200px;padding-top:50px;height: 150px;background: #ced;}div button{margin:0 auto;display: block;}</style> </head> <body> <div> <button id="button">按鈕1</button> </div> <script> let div = document.getElementsByTagName('div')[0], btn = document.getElementsByTagName('button')[0]; $.event.add(div,'click',()=>console.log('div普通單擊事件')); //給div綁定一個click事件 $.event.add(div,'click',()=>console.log('d1代理事件'),null,'button'); //給div綁定一個代理事件,監聽對象爲button </script> </body> </html>
渲染以下:源碼分析
咱們給div綁定了一個普通事件(類型爲click),還有一個代理事件(代理的元素是button),當咱們點擊div元素時將觸發普通事件,以下:
代理事件並無被觸發,當咱們點擊按鈕1時將會同時觸發普通事件和代理事件:
這裏的代理事件是在jQuery內部實現的,而普通事件是由於原生的冒泡的事件流所產生的,
源碼分析
jQuery內部的事件綁定也是經過原生的addEventListener或attachEvent來實現的,不過jQuery對這個過程作了優化,它不僅是僅僅的調用該API實現綁定,用jQuery綁定事件時,在同一個DOM元素上的綁定的任何事件,其實最後綁定的都是同一個函數,該函數只有幾行diamagnetic,最後又會調用jQuery.event.dispatch去進行事件的分發,並執行事件監聽函數。
上面說了事件系統模塊是基於數據緩存模塊來管理監聽函數的,咱們經過jQuery綁定的事件都保存到了$. cache裏對應的DOM元素的數據緩存對象上(有疑問的能夠看下數據緩存模塊,前面介紹過了),好比上面的栗子,咱們打印一下$.cache能夠看到綁定的信息:
每一個DOM元素在內部數據緩存對上有兩個屬性是和事件有關的:
好了,如今來講一下源碼實現,$.event.add的源碼實現以下:
jQuery.event = { add: function( elem, types, handler, data, selector ) { //綁定一個或多個類型的事件監聽函數 var elemData, eventHandle, events, t, tns, type, namespaces, handleObj, handleObjIn, quick, handlers, special; // 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; } // Caller can pass in an object of custom data in lieu of the handler if ( handler.handler ) { //若是參數handle是自定義監聽對象,其中的屬性價會被設置到後面所建立的新監聽對象上。函數cloneCopyEvent(src,dest)將調用該方法 handleObjIn = handler; handler = handleObjIn.handler; } // Make sure that the handler has a unique ID, used to find/remove it later if ( !handler.guid ) { //若是函數handler沒有guid handler.guid = jQuery.guid++; //則爲它分配一個惟一標識guid,在移除監聽函數時,將經過這個惟一標識來匹配監聽函數。 } // Init the element's event structure and main handler, if this is the first events = elemData.events; //嘗試取出事件緩存對象 if ( !events ) { //若是不存在,表示從未在當前元素上(經過jQuery事件方法)綁定過事件, elemData.events = events = {}; //則把它初始化爲一個空對象。events對象保存了存放當前元素關聯的全部監聽函數。 } eventHandle = elemData.handle; //嘗試取出主監聽函數handle(event) if ( !eventHandle ) { elemData.handle = eventHandle = function( e ) { //若是不存在,表示從未在當前元素上(jQuery事件方法)綁定過事件,則初始化一個主監聽函數,並把它存儲到事件緩存對象的handle屬性上,這是事件觸發時真正執行的函數 // 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) ? 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; } // Handle multiple events separated by a space // jQuery(...).bind("mouseover mouseout", fn); types = jQuery.trim( hoverHack(types) ).split( " " ); //先調用hoverHack()函數修正hover.namespace類型的事件,再調用split把types用空格進行分隔,轉換爲一個數組 for ( t = 0; t < types.length; t++ ) { //遍歷須要綁定的每一個事件類型,逐個綁定事件 tns = rtypenamespace.exec( types[t] ) || []; //正則rtypenamespace用於解析事件類型和命名空間,執行後tns[1]是事件類型,tns[2]是一個或多個命名空間,用.分隔 type = tns[1]; //type就是事件類型 namespaces = ( tns[2] || "" ).split( "." ).sort(); // If event changes its type, use the special event handlers for the changed type special = jQuery.event.special[ type ] || {}; // If selector defined, determine special event api type, otherwise given type type = ( selector ? special.delegateType : special.bindType ) || type; // Update special based on newly reset type special = jQuery.event.special[ type ] || {}; // handleObj is passed to all event handlers handleObj = jQuery.extend({ //把監聽函數封裝爲監聽對象,用來支持事件模擬、自定義事件數據等。 type: type, //實際使用的事件類型,不包含命名空間,可能被修改過 origType: tns[1], //原始事件類型,不包含命名空件,未通過修正 data: data, //排序後的命名空間,若是傳入的事件類型是'click.a.c.b',那麼namespace就是a.b.c handler: handler, //傳入的監聽函數 guid: handler.guid, //分配給監聽函數的惟一標識guid selector: selector, //自定義的事件數據 quick: quickParse( selector ), //入的事件代理選擇器表達式,當代理事件被觸發時,用該屬性過濾代理元素的後代元素。 namespace: namespaces.join(".") }, handleObjIn ); // Init the event handler queue if we're the first handlers = events[ type ]; //嘗試取出事件類型type對應的監聽對象數組handlers,其中存放了已綁定的監聽對象。 if ( !handlers ) { //若是type事件類型的數組不存在,則進行綁定操做 handlers = events[ type ] = []; //把監聽對象數組的handlers初始化爲一個空數組。 handlers.delegateCount = 0; //初始化handlers.delegateCount爲0,表示代理事件的個數爲0 // Only use addEventListener/attachEvent if the special events handler returns false if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { //綁定主監聽函數 優先調用修正對象的修正方法setup() // Bind the global event handler to the element if ( elem.addEventListener ) { //若是瀏覽器支持addEventListener()方法 elem.addEventListener( type, eventHandle, false ); //調用原生方法addEventListener()綁定主監聽函數,以冒泡流的方式。 } else if ( elem.attachEvent ) { //若是沒有addEventListener() elem.attachEvent( "on" + type, eventHandle ); //則調用attachEvent()方法綁定主監聽函數,IE8及更早的瀏覽器只支持冒泡 } } } if ( special.add ) { //若是修正對象有修正方法add() special.add.call( elem, handleObj ); //則先調用修正方法add()綁定監聽函數 if ( !handleObj.handler.guid ) { handleObj.handler.guid = handler.guid; } } // Add to the element's handler list, delegates in front if ( selector ) { //若是傳入了selector參數,則綁定的是代理事件 handlers.splice( handlers.delegateCount++, 0, handleObj ); //把代理監聽對象插入屬性handlers.delegateCount所指定的位置。每次插入代理監聽對象後,監聽對象數組的屬性handlers.delegateCount自動加1,以指示下一個代理監聽對象的插入位置。 } else { //未傳入selector參數狀況下,則是普通的事件綁定 handlers.push( handleObj ); //把監聽對象插入末尾 } // Keep track of which events have ever been used, for event optimization jQuery.event.global[ type ] = true; //記錄綁定過的事件類型 } // Nullify elem to prevent memory leaks in IE elem = null; //解除參數elem對DOM元素的引用,以免內存泄漏(IE) }, /*略*/ }
$.event.add會經過addEventListener或attachEvent去綁定事件,當事件被觸發時就會執行咱們綁定的事件,也就是上面的elemData.handle函數,該函數會執行jQuery.event.dispatch函數,jQuery.event.dispatch就是用於分發事件的,以下:
jQuery.event = { dispatch: function( event ) { //分發事件,執行事件監聽函數 // Make a writable jQuery.Event from the native event object event = jQuery.event.fix( event || window.event ); //調用jQuery.event.fix(event)把原生事件對象封裝爲jQuery事件對象。 var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), //取出this元素當前事件類型對應的函數列表。 delegateCount = handlers.delegateCount, //代理監聽對象個事 args = [].slice.call( arguments, 0 ), //把參數arguments轉換爲真正的數組 run_all = !event.exclusive && !event.namespace, handlerQueue = [], i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; // Use the fix-ed jQuery.Event rather than the (read-only) native event args[0] = event; //將event保存爲args[0],監聽函數執行時這是做爲第一個參數傳入的 event.delegateTarget = this; //當前的DOM對象,等於event.currentTarget屬性的值 // Determine handlers that should run if there are delegated events // Avoid disabled elements in IE (#6911) and non-left-click bubbling in Firefox (#3861) if ( delegateCount && !event.target.disabled && !(event.button && event.type === "click") ) { //若是當前對象綁定了代理事件,且目標對象target沒有禁用 !(event.button && event.type === "click")的意思是:是鼠標單擊時 // Pregenerate a single jQuery object for reuse with .is() jqcur = jQuery(this); //用當前元素構造一個jQuery對象,以便在後面的代碼中複用它的方法.is(selector) jqcur.context = this.ownerDocument || this; //上下文 for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { //從事件目標開始,再依次到父節點,一直到當前目標爲止,遍歷從觸發事件的元素到代理元素這條路徑上的全部後代元素。 selMatch = {}; //重置selMatch爲空對象 matches = []; //重置matches爲空數組 jqcur[0] = cur; //將jqcur綁定到每個後代元素 for ( i = 0; i < delegateCount; i++ ) { //遍歷全部的代理監聽對象數組。 handleObj = handlers[ i ]; //某個代理監聽對象。 sel = handleObj.selector; //當前代理監聽對象的selector屬性 if ( selMatch[ sel ] === undefined ) { //若是綁定的事件對應的選擇器在selMatch中不存在,則進行檢查,看是否符合要求 selMatch[ sel ] = ( handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) ); } if ( selMatch[ sel ] ) { //當後代元素與代理監聽對象的選擇器表達式匹配時 matches.push( handleObj ); //把代理監聽對象放入數組matches中。 } } if ( matches.length ) { handlerQueue.push({ elem: cur, matches: matches }); //最後把某個後代元素匹配的全部代理監聽對象代理放到數組handlerQueue裏。 } } } // Add the remaining (directly-bound) handlers if ( handlers.length > delegateCount ) { //若是監聽對象數組handlers的長度大於代理監聽對象的位置計數器delegateCount,則表示爲當前元素綁定了普通事件 handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); //把這些監聽對象也放入待執行隊列handlerQueue中 } // Run delegates first; they may want to stop propagation beneath us for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { //執行數組handlerQueue中的全部函數。遍歷待執行隊列handlerQueue,若是某個元素的監聽函數調用了方法stopPropagation()則終止for循環 matched = handlerQueue[ i ]; //matched是數組handlerQueue中的一個對象 event.currentTarget = matched.elem; //把當前正在執行監聽函數的元素賦值給事件屬性event.currentTarget。這樣在事件處理函數內,this等於綁定的代理函數 for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { //遍歷元素定對應的監聽對象數組,並執行監聽對象中的監聽函數 handleObj = matched.matches[ j ]; //handleObj是一個具體的監聽對象handleObj // Triggered event must either 1) be non-exclusive and have no namespace, or // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { event.data = handleObj.data; //把監聽對象的屬性handleObj.data賦值給jQuery事件對象 event.handleObj = handleObj; //把監聽對象賦值給jQuery事件對象event.handleObj上,這樣監聽函數就能夠經過該屬性訪問監聽對象上的諸多屬性 ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) //優先調用修正對象的修正方法special.handle(), .apply( matched.elem, args ); if ( ret !== undefined ) { //若是函數有返回值 event.result = ret; if ( ret === false ) { //若是監聽對象的返回值是false event.preventDefault(); //調用preventDefault()方法阻止默認行爲 event.stopPropagation(); //stopPropagation()方法中止事件傳播 } } } } } return event.result; }, /*略*/ }
jQuery.event.fix()會把原生事件修正爲jQuery事件對象並返回,修正不兼容屬性,就是自定義一個對象,保存該事件的信息,好比:類型、建立的時間、原生事件對象等,有興趣的能夠調試一下。