1. 程式人生 > >jQuery原始碼分析--event事件繫結(上)

jQuery原始碼分析--event事件繫結(上)

上文提到,jquery的事件繫結有bind(),delegate()和one()以及live()方式。我用的jQuery2.1.3版本,live()已經被廢棄了。

bind(),delegate()和one()的內部原始碼。

//7491行
bind: function( types, data, fn ) {
        return this.on( types, null, data, fn );
    },
//7498行
delegate: function( selector, types, data, fn ) {
        return this.on( types, selector, data, fn );
    },
//7474行 
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 ) { // Handle event binding jQuery.fn[ name ] = function
( data, fn ) {
return arguments.length > 0 ? this.on( name, null, data, fn ) : this.trigger( name ); }; }); //4859行 one: function( types, selector, data, fn ) { return this.on( types, selector, data, fn, 1 ); },

以下這幾個函式全都指向了on方法。

jQuery.fn.on

//4806行
on: function( types, selector, data, fn, /*INTERNAL*/ one ) { var origFn, type; // Types can be a map of types/handlers // types可以是一個由types/handlers組成的map物件 if ( typeof types === "object" ) { // ( types-Object, selector, data ) // 如果selector不是字串 // 則將傳參由( types-Object, selector, data )變成( types-Object, data ) if ( typeof selector !== "string" ) { // ( types-Object, data ) data = data || selector; selector = undefined; } //遍歷所有type for ( type in types ) { //新增type事件處理函式 this.on( type, selector, data, types[ type ], one ); } return this; } // 如果data為空,且fn為空 if ( data == null && fn == null ) { // 傳參變成( types, fn ) fn = selector; data = selector = undefined; // 否則如果只是fn為空 } else if ( fn == null ) { // 如果selector為字串 if ( typeof selector === "string" ) { // 傳參變成( types, selector, fn ) fn = data; data = undefined; } else { // 否則傳參變成( types, data, fn ) fn = data; data = selector; selector = undefined; } } if ( fn === false ) { //如果fn為false則變成一個return false的函式 fn = returnFalse; } else if ( !fn ) { //如果fn現在還不存在,則直接return this return this; } // 如果one為1,one引數為1說明是函式只執行一次即被廢除 if ( one === 1 ) { //把fn賦值給另一個變數儲存 origFn = fn; //重新定義fn fn = function( event ) { // Can use an empty set, since event contains the info // 這個事件只用一次,用完就用off取消掉。 jQuery().off( event ); return origFn.apply( this, arguments ); }; // Use same guid so caller can remove using origFn // 使用相同的ID,為了未來好刪除事件 fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); } //最後,對於所有情況,呼叫jQuery.event.add函式繼續處理 return this.each( function() { jQuery.event.add( this, types, fn, data, selector ); }); },

可見,jquery.fn.on函式有以下功能

  • 對每個不同的輸入情況進行過載
  • 過載之後交給jQuery.event.add函式處理

總的來說就是:通過 on 繫結事件,分析傳遞的資料,加工變成 add 能夠識別的資料。

jQuery.event.add

//4083行
add: function( elem, types, handler, data, selector ) {

        var handleObjIn, eventHandle, tmp,
            events, t, handleObj,
            special, handlers, type, namespaces, origType,
            // 通過內部快取獲取元素資料
            elemData = data_priv.get( elem );

        // Don't attach events to noData or text/comment nodes (but allow plain objects)
        //不要把事件新增到沒有資料或沒有文字的節點(但允許普通的節點)
        if ( !elemData ) {
            return;
        }

        // Caller can pass in an object of custom data in lieu of the handler
        //in lieu of 代替
        //定位handler的兩個引數handler和selector
        if ( handler.handler ) {
            handleObjIn = handler;
            handler = handleObjIn.handler;
            selector = handleObjIn.selector;
        }

        // Make sure that the handler has a unique ID, used to find/remove it later
        //如果handler沒有id,則給它定義一個id,確保每個handler都有一個id,用於未來查詢和刪除
        if ( !handler.guid ) {
            handler.guid = jQuery.guid++;
        }

        // Init the element's event structure and main handler, if this is the first
        //如果是第一次建立,初始化元素的事件結構和主要handler
        // 如果快取資料中沒有events資料
        if ( !(events = elemData.events) ) {
            // 則初始化events
            events = elemData.events = {};
        }
         // 如果快取資料中沒有handle資料
        if ( !(eventHandle = elemData.handle) ) {
            // 定義事件處理函式
            eventHandle = elemData.handle = function( e ) {
                // Discard the second event of a jQuery.event.trigger() and
                // when an event is called after a page has unloaded
                // 丟棄jQuery.event.trigger第二次觸發事件以及當一個頁面被解除安裝後呼叫事件
                return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ?
                    jQuery.event.dispatch.apply( elem, arguments ) : undefined;
            };
        }

        // Handle multiple events separated by a space
        // 事件可能是通過空格鍵分隔的字串,所以將其變成字串陣列
        types = ( types || "" ).match( rnotwhite ) || [ "" ];
          // 事件的長度
        t = types.length;
          // 遍歷所有事件
        while ( t-- ) {
            //嘗試取出事件的名稱空間,就如上文提到的click.plugin,plugin是名稱空間
            tmp = rtypenamespace.exec( types[t] ) || [];
            //取出事件
            type = origType = tmp[1];
            //取出名稱空間,通過“.”分割成陣列
            namespaces = ( tmp[2] || "" ).split( "." ).sort();

            // There *must* be a type, no attaching namespace-only handlers
            //必須要有事件型別
            if ( !type ) {
                continue;
            }

            // 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
            //如果定義了selector,決定選擇哪個特殊事件api;如果沒有selector,就使用type了
            type = ( selector ? special.delegateType : special.bindType ) || type;

            // Update special based on newly reset type
            //更新特殊事件的type
            special = jQuery.event.special[ type ] || {};


            // handleObj is passed to all event handlers
            //組裝用於特殊事件處理的物件
            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 );

            // Init the event handler queue if we're the first
            //如果第一次使用,初始化事件處理佇列
            if ( !(handlers = events[ type ]) ) {
                handlers = events[ type ] = [];
                handlers.delegateCount = 0;

                // Only use addEventListener if the special events handler returns false
                //如果特殊事件returnfalse,獲取失敗,就用addEventListener方法啦
                if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
                    if ( elem.addEventListener ) {
                        elem.addEventListener( type, eventHandle, false );
                    }
                }
            }
            // 通過特殊事件add處理事件
            if ( special.add ) {
                // 新增事件
                special.add.call( elem, handleObj );
                 // 設定處理函式的ID
                if ( !handleObj.handler.guid ) {
                    handleObj.handler.guid = handler.guid;
                }
            }
            //將事件新增到事件列表handlers,delegate事件(委託事件)放到列表頭部,其他事件放到尾部
            // Add to the element's handler list, delegates in front
            if ( selector ) {
                handlers.splice( handlers.delegateCount++, 0, handleObj );
            } else {
                handlers.push( handleObj );
            }

            // Keep track of which events have ever been used, for event optimization
            //跟蹤事件已被使用,用於事件優化
            jQuery.event.global[ type ] = true;
        }

    },

jQuery.event.add函式以下功能:

// 通過內部快取獲取元素資料
            elemData = data_priv.get( elem );
  • 獲取資料快取。如果事件被註冊過,用get方法從快取中拿。如果快取中沒有,就建立一個用途elem對映的快取區,這個處理主要是合併同個元素繫結多個事件的問題。就是把相同元素繫結的不同行為都合併到一個快取裡。
    // Make sure that the handler has a unique ID, used to find/remove it later
        //如果handler沒有id,則給它定義一個id,確保每個handler都有一個id,用於未來查詢和刪除
        if ( !handler.guid ) {
            handler.guid = jQuery.guid++;
        }
  • 建立編號 新增編號的目的是用來尋找和刪除事件,因為事件是儲存在快取裡的,並沒有與元素直接關聯。
        // Init the element's event structure and main handler, if this is the first
        //如果是第一次建立,初始化元素的事件結構和主要handler
        // 如果快取資料中沒有events資料
        if ( !(events = elemData.events) ) {
            // 則初始化events
            events = elemData.events = {};
        }
         // 如果快取資料中沒有handle資料
        if ( !(eventHandle = elemData.handle) ) {
            // 定義事件處理函式
            eventHandle = elemData.handle = function( e ) {
                // Discard the second event of a jQuery.event.trigger() and
                // when an event is called after a page has unloaded
                // 丟棄jQuery.event.trigger第二次觸發事件以及當一個頁面被解除安裝後呼叫事件
                return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ?
                    jQuery.event.dispatch.apply( elem, arguments ) : undefined;
            };
        }
//eleData結構
elemData = {
       events:{}
       eventHandle:function(){}
}
  • 給快取elemData增加屬性 events是內部維護的事件佇列,eventHandle 是作用到addeventlistener的回撥函式。這裡把回撥函式設為jQuery.event.dispatch.apply( elem, arguments )。
  • 填充事件名與事件控制代碼 取出名稱空間,事件型別等
  • 區分了特殊事件和普通事件。為特殊事件進行了組裝。並引用了鉤子處理特殊事件。
  • 引入了一個handlers事件處理list,把事件全部放到list裡一個個執行,而且還區分了delegate事件和其他事件,delegate事件放在list頭,其他事件放在list尾。並未delegate事件添加了delegateCount屬性,委託次數。
  • 最後執行addeventlistener函式,callback函式是jQuery.event.dispatch.apply( elem, arguments )方法

總的來說就是:
1. 通過 add 把資料整理放到資料快取中儲存,通過 addEventListener 繫結事件
2. 觸發事件執行 addEventListener 回撥 dispatch 方法

jquery2.1.3沒有做IE8以前的事件相容,預設放棄了IE8以前的版本,沒有做attachEvent事件的相容處理。

jQuery.event.dispatch


// 4391行
dispatch: function( event ) {

        // Make a writable jQuery.Event from the native event object
        //  重寫原生事件物件,變成一個可讀寫的物件,方便未來修改、擴充套件
        event = jQuery.event.fix( event );

        var i, j, ret, matched, handleObj,
            handlerQueue = [],
            //引數轉陣列
            args = slice.call( arguments ),
             // 從內部資料中查詢該元素的對應事件處理器列表中的對應處理器,否則為空陣列
            handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [],
            // 嘗試將事件轉成特殊事件
            special = jQuery.event.special[ event.type ] || {};

        // Use the fix-ed jQuery.Event rather than the (read-only) native event
        // 將引數陣列第一個元素換成重寫的事件物件
        args[0] = event;
        event.delegateTarget = this;

        // Call the preDispatch hook for the mapped type, and let it bail if desired
        // 嘗試使用特殊事件的preDispatch鉤子來繫結事件,並在必要時退出
        if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
            return;
        }

        // Determine handlers
         // 組裝事件處理包{elem, handlerObjs}(這裡是各種不同元素)的佇列。
        handlerQueue = jQuery.event.handlers.call( this, event, handlers );

        // Run delegates first; they may want to stop propagation beneath us
        i = 0;
        // 遍歷事件處理包{elem, handlerObjs}(取出來則對應一個包了),且事件不需要阻止冒泡
        while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
        // 定義當前Target為事件處理物件對應的元素
            event.currentTarget = matched.elem;

            j = 0;
            // 如果事件處理物件{handleObjs}存在(一個元素可能有很多handleObjs),且事件不需要立刻阻止冒泡
            while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {

                // Triggered event must either 1) have no namespace, or 2) have namespace(s)
                // a subset or equal to those in the bound event (both can have no namespace).
                // 觸發的事件必須滿足其一:
                // 1) 沒有名稱空間
                // 2) 有名稱空間,且被繫結的事件是名稱空間的一個子集
                if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {

                    event.handleObj = handleObj;
                    event.data = handleObj.data;
// 嘗試通過特殊事件獲取處理函式,否則使用handleObj中儲存的handler(所以handleObj中還儲存有handler)
                    ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
                            .apply( matched.elem, args );
                   // 如果處理函式存在
                    if ( ret !== undefined ) {
                     // 如果處理函式返回值是false,則阻止冒泡,阻止預設動作
                        if ( (event.result = ret) === false ) {
                            event.preventDefault();
                            event.stopPropagation();
                        }
                    }
                }
            }
        }

        // Call the postDispatch hook for the mapped type
        // 嘗試通過special.postDispatch勾住這個對映關係,未來可以優化
        if ( special.postDispatch ) {
            special.postDispatch.call( this, event );
        }
       // 返回事件函式
        return event.result;
    },

這裡首先就呼叫了 jQuery.event.fix( event )函式對事件做了相容處理。

jQuery.event.fix

//4496行  fix函式以及fix函式用到的函式們
// Includes some event props shared by KeyEvent and MouseEvent
    //props 儲存了原生事件物件 event 的通用屬性
    props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),

    fixHooks: {},
    //keyHook.props 儲存鍵盤事件的特有屬性
    keyHooks: {
        props: "char charCode key keyCode".split(" "),
        filter: function( event, original ) {

            // Add which for key events
            if ( event.which == null ) {
                event.which = original.charCode != null ? original.charCode : original.keyCode;
            }

            return event;
        }
    },
    //mouseHooks.props 儲存滑鼠事件的特有屬性。
    mouseHooks: {
        props: "button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),
        //keyHooks.filter 和 mouseHooks.filter 兩個方法分別用於修改鍵盤和滑鼠事件的屬性相容性問題,用於統一介面。
        filter: function( event, original ) {
            var eventDoc, doc, body,
                button = original.button;

            // Calculate pageX/Y if missing and clientX/Y available
            if ( event.pageX == null && original.clientX != null ) {
                eventDoc = event.target.ownerDocument || document;
                doc = eventDoc.documentElement;
                body = eventDoc.body;

                event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
                event.pageY = original.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );
            }

            // Add which for click: 1 === left; 2 === middle; 3 === right
            // Note: button is not normalized, so don't use it
            if ( !event.which && button !== undefined ) {
                event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
            }

            return event;
        }
    },
 //大家看過來啦,,fix函式開始了
    fix: function( event ) {
        if ( event[ jQuery.expando ] ) {
            return event;
        }

        // Create a writable copy of the event object and normalize some properties
        var i, prop, copy,
            type = event.type,
            originalEvent = event,
            fixHook = this.fixHooks[ type ];
         //判斷事件的型別,是mouse事件還是key事件
        if ( !fixHook ) {
            this.fixHooks[ type ] = fixHook =
                rmouseEvent.test( type ) ? this.mouseHooks :
                rkeyEvent.test( type ) ? this.keyHooks :
                {};
        }
        //實際上就是判斷事件是mouse事件還是key事件,如果是mouse事件把props和mouse事件的props都加到事件的屬性裡
        copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;

        event = new jQuery.Event( originalEvent );
//jQuery 自己寫了一個基於 native event 的 Event 物件,並且把 copy 陣列中對應的屬性從 native event 中複製到自己的 Event 物件中。
        i = copy.length;
        while ( i-- ) {
            prop = copy[ i ];
            event[ prop ] = originalEvent[ prop ];
        }

        // Support: Cordova 2.5 (WebKit) (#13255)
        // All events should have a target; Cordova deviceready doesn't
        if ( !event.target ) {
            event.target = document;
        }

        // Support: Safari 6.0+, Chrome<28
        // Target should not be a text node (#504, #13143)
        if ( event.target.nodeType === 3 ) {
            event.target = event.target.parentNode;
        }
        //在最後 jQuery 還不忘放一個鉤子,呼叫 fixHook.fitler 方法用以糾正一些特定的 event 屬性。例如 mouse event 中的 pageX,pageY,keyboard event中的 which,進一步修正事件物件屬性的相容問題。
        return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;
    },

fix函式實際上就是將js原生事件的通用屬性重寫了一遍,附加到jquery自己建立的事件物件上面了。
為什麼將瀏覽器原生 Event 的屬性賦值到新建立的 jQuery.Event 物件中去哪?jQuery要增加自己的處理機制,這樣更靈活,而且還可以傳遞 data 資料,也就是使用者自定義的資料。

fix函式總結:

1.將原生的事件物件 event 修正為一個新的可寫 event 物件,並對該 event 的屬性以及方法統一介面
2.該方法在內部呼叫了 jQuery.Event(event) 建構函式

回到depatch函式,呼叫fix函式之後

  • 嘗試把事件變成special事件
  • 呼叫 jQuery.event.handlers.call( this, event, handlers );函式組裝事件處理包的佇列handlerQueue。這裡是不同元素的佇列。相同元素的事件佇列見前面elemData。
  • 遍歷事件處理包,滿足條件的進行觸發。這裡對callback函式return false的處理是既阻止冒泡也阻止事件的進一步傳播。

jQuery.event.handlers

//4450行
handlers: function( event, handlers ) {
        var i, matches, sel, handleObj,
            handlerQueue = [],
            delegateCount = handlers.delegateCount,
            //獲取事件的觸發元素
            cur = event.target;

        // Find delegate handlers
        // Black-hole SVG <use> instance trees (#13180)
        // Avoid non-left-click bubbling in Firefox (#3861)
        //如果有delegateCount屬性,代表事件時delegate型別事件(即事件委託)
        // 找出所有delegate的處理函式列隊
        if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) {
           // 遍歷元素及元素父級節點
            for ( ; cur !== this; cur = cur.parentNode || this ) {

                // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
                // 防止單機被禁用的元素時觸發事件
                if ( cur.disabled !== true || event.type !== "click" ) {
                // 開始組裝符合要求的事件處理物件
                    matches = [];
                     // 遍歷所有事件處理物件,還記得嗎,add方法裡面,delegate型別的事件都放在handlers佇列的最前面了
                    for ( i = 0; i < delegateCount; i++ ) {
                        handleObj = handlers[ i ];

                        // Don't conflict with Object.prototype properties (#13203)                // 選擇器,用於過濾
                        sel = handleObj.selector + " ";
                        // 如果matches上沒有繫結該選擇器數量
                        if ( matches[ sel ] === undefined ) {
                            // 在matches上繫結該選擇器數量
                            matches[ sel ] = handleObj.needsContext ?
                                // 得出選擇器數量,並賦值
                                jQuery( sel, this ).index( cur ) >= 0 :
                                jQuery.find( sel, this, null, [ cur ] ).length;
                        }
                        // 再次確定是否繫結選擇器數量
                        if ( matches[ sel ] ) {
                            // 是則將事件處理物件推入
                            matches.push( handleObj );
                        }
                    }
                    // 如果得到的matches裡有事件處理物件
                    if ( matches.length ) {
                    // 組裝成事件處理包(暫時這麼叫吧),推入事件處理包隊
                        handlerQueue.push({ elem: cur, handlers: matches });
                    }
                }
            }
        }

        // Add the remaining (directly-bound) handlers
        // 如果還有事件剩餘,則將剩餘的裝包,推入列隊
        if ( delegateCount < handlers.length ) {
            handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });
        }

        return handlerQueue;
    },

handlers函式有什麼作用呢?
現在有一個問題。假設一段jquery程式碼是這樣的。


<div id="aaron">
   <div id='test'>
        <ul>
            <p>點選測試委託順序</p>
        </ul>
   </div>
</div>
var ul = $('ul')
function show(data){
  ul.append('<li>'+ data +'</li>')
}

var aaron = $("#aaron")
//同一個元素上繫結不同的事件委託
aaron.on('mousedown','p',function(e){
    show('p')
   e.stopPropagation()
})
aaron.on('mousedown','ul',function(e){
    show('被阻止了')
})
$("#test").on('mousedown',function(){
  show('test')
})

執行結果:
text p

發現“被阻止了”沒有打印出來。

arron有兩個事件委託。一個是子孫元素p元素委託arron,另一個是ul元素委託。當p元素委託之後,callback函式裡明確規定stopPropagation,這時ul元素繫結的事件不能觸發。

handlers函式就是為了實現這個目標噠。

handlers有如下需求
- 根據冒泡的原理,不管事件新增的順序如何,應該為elem上的所有事件依照觸發順序排出一個層次來,最裡的委託優先順序最高,最外的委託優先順序最低,這樣停止冒泡的需求得以實現。

handlers實現原理

  • 之前add方法對每個elem快取了一個數據結構elemData,還有一個區分有沒有事件委託的變數delegateCount。現在只需根據delegateCount判斷元素有沒有事件委託。有事件委託,則對elemData裡面的事件佇列做一個排序,按照委託的節點的DOM深度排序,深度越深,排序越靠前,深度越低,排序越靠後。
  • 即通過 target 與實際的事件繫結物件我們就可以劃分一個區域段,通過遞迴獲取每一個元素的 parentNode 節點,在每一個節點層上通過與委託節點的對比用來確定是不是委託的事件元素,這個就是委託的核心思路了。

還有一個special事件的一堆函式,下次再寫嘍。