jQuery事件系統并沒有將事件監聽函式直接系結到DOM元素上,而是基于資料快取模塊來管理監聽函式的,事件模塊代碼有點多,我把它分為了三個部分:分底層方法、實體方法和便捷方法、ready事件來講,好理解一點,
jQuery的事件分為普通事件和代理事件:
- 普通事件 ;當我們再div上定義一個click事件,此時如果點擊div或按鈕都會觸發該普通事件,這是由于冒泡的緣故
- 代理事件 ;當我們在div上定義一個代理事件,且selector設定為button時,我們點擊div將不會觸發該事件,只有點擊了這個按鈕才會觸發這個代理事件
事件系統模塊的底層方法如下:
- $.event.add(elem,types,handler,data,selector) ;系結一個或多個型別的事件監聽函式,引數如下:
- elem ;操作的元素
- types ;系結的事件型別,多個事件型別之間用空格隔開,
- handler ;待系結的事件監聽函式,也可以是一個自定義的監聽物件,
- data ;自定義資料,
- selector ;選擇器運算式字串,用于系結代理事件, ;
- $.event.remove(elem, types, handler, selector, mappedTypes) ;移除DOM元素上系結的一個或多個型別的事件監聽函式,引數同$.event.add(),如果只傳入一個elem元素則移除該元素上的所有事件,
- $.event.trigger(type,data,elem,onlyHandlers) ;手動觸發事件,執行系結的事件監聽函式和默認行為,并且會模擬冒泡程序,引數如下:
- type ;事件字串
- data ;傳遞給回應函式的資料
- elem ;觸發該事件的DOM物件
- onlyHandlers ;是否只觸發elem元素對應的事件監聽函式,而不冒泡
常用的就是以上三個吧,其它還有$.event.global(記錄系結過的事件)、$.event.removeEvent(用于洗掉事件)等
先舉栗子前先先簡單說一下jQuery里的事件的分類,jQuery的事件分為普通事件和代理事件:
- 普通事件 ;直接系結在這個元素的某個事件型別上,當在該元素上觸發了這個事件時,則執行該事件
- 代理事件 ;當事件直接發生在代理元素上時,監聽函式不會執行,只有當事件從后代元素冒泡到代理元素上時,才會用引數selector匹配冒泡路上的后代元素,然后用匹配成功的后代元素作為背景關系去執行監聽函式,
這樣說可能理解不了,舉個栗子,我們定義兩個DOM元素先

這里我們定義了一個div,內部含有一個子節點button,渲染如下:

舉個栗子:
<!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元素在內部資料快取對上有兩個屬性是和事件有關的:
- events ;屬性是一個物件,其中存盤了該DOM元素的所有事件,該物件的下的每個元素的元素名是事件型別,值是一個陣列,是封裝了監聽函式的handleObject集合
- handle ;DOM元素的主監聽函式,負責分發事件和執行監聽函式,對于一個DOM元素,jQuery事件系統只會為之分配一個主監聽函式,所有型別的事件都被系結到這個主監聽函式
好了,現在來說一下原始碼實作,$.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 = https://www.cnblogs.com/greatdesert/p/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事件物件并回傳,修正不兼容屬性,就是自定義一個物件,保存該事件的資訊,比如:型別、創建的時間、原生事件物件等,有興趣的可以除錯一下,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/34137.html
標籤:jQuery
上一篇:layUI
