jQuery 源碼分析(十六) 事件系統模塊 底層方法 詳解

jQuery事件系統並無將事件監聽函數直接綁定到DOM元素上,而是基於數據緩存模塊來管理監聽函數的,事件模塊代碼有點多,我把它分爲了三個部分:分底層方法、實例方法和便捷方法、ready事件來說,好理解一點。html

jQuery的事件分爲普通事件和代理事件:node

  • 普通事件  ;當咱們再div上定義一個click事件,此時若是點擊div或按鈕都會觸發該普通事件,這是因爲冒泡的緣故
  • 代理事件  ;當咱們在div上定義一個代理事件,且selector設置爲button時,咱們點擊div將不會觸發該事件,只有點擊了這個按鈕纔會觸發這個代理事件

事件系統模塊的底層方法以下:jquery

  • $.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(用於刪除事件)等api

先舉栗子前先先簡單說一下jQuery裏的事件的分類,jQuery的事件分爲普通事件和代理事件:數組

  • 普通事件  ;直接綁定在這個元素的某個事件類型上,當在該元素上觸發了這個事件時,則執行該事件
  • 代理事件  ;當事件直接發生在代理元素上時,監聽函數不會執行,只有當事件從後代元素冒泡到代理元素上時,纔會用參數selector匹配冒泡路上的後代元素,而後用匹配成功的後代元素做爲上下文去執行監聽函數。

這樣說可能理解不了,舉個栗子,咱們定義兩個DOM元素先瀏覽器

這裏咱們定義了一個div,內部含有一個子節點button,渲染以下:緩存

舉個栗子:app

writer by:大沙漠 QQ:22969969函數

<!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 = 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事件對象並返回,修正不兼容屬性,就是自定義一個對象,保存該事件的信息,好比:類型、建立的時間、原生事件對象等,有興趣的能夠調試一下。

相關文章
相關標籤/搜索