jQuery源碼分析--Event模塊(1)

  jQuery的Event模塊提供了強大的功能:事件代理,自定義事件,自定義數據等。今天記錄一下它實現的原理。html

  咱們都知道,在js的原生事件中,有事件對象和回調函數這兩樣東西。可是事件對象是隻讀的,因此jQuery就用了本身的Event對象替代了原生的事件對象,這樣就能夠實現對事件對象的徹底控制,因此才能實現自定義數據。而回調函數的話,每一個元素只有一個同樣的回調函數,這樣方便管理。node

  1. 下面來看看event對象長什麼樣。


    能夠看到jQuery的事件對象其實一開始就只有這麼一點東西。
    其中originalEvent是原生事件對象副本。
    jQuery211030632698768749833則是一個標誌,之後能夠用這個標誌來判斷這個對象是否是jQuery的事件對象。

  2. 緊接着咱們看一下Event對象的原型。

    能夠看到有六個個方法,前三個是用來判斷是否已經被阻止默認行爲、是否已經被阻止冒泡和默認行爲、是否已經被阻止冒泡。
    後三個則是相應的操做。
    上源代碼:
    jQuery.Event = function( src, props ) {//src能夠是原生事件類型、jquery事件類型、自定義事件類型、原生事件對象
        // Allow instantiation without the 'new' keyword 不用new關鍵字實例化jquery的事件對象
        if ( !(this instanceof jQuery.Event) ) {
            return new jQuery.Event( src, props );
        }
    
        // Event object
        if ( src && src.type ) {//若是是原生事件對象或jquery事件類型
            this.originalEvent = src;//保存原生事件對象 
            this.type = src.type;//事件類型
    
            // Events bubbling up the document may have been marked as prevented
            // by a handler lower down the tree; reflect the correct value.
            this.isDefaultPrevented = src.defaultPrevented ||//是否被更底層的事件阻止默認行爲
                    src.defaultPrevented === undefined &&
                    // Support: Android < 4.0
                    src.returnValue === false ?
                returnTrue :
                returnFalse;
    
        // Event type
        } else {//原生事件類型、自定義事件類型
            this.type = src;
        }
    
        // Put explicitly provided properties onto the event object
        if ( props ) {//若是傳入了自定義的props對象,將其複製到jQuery.Event對象上
            jQuery.extend( this, props );
        }
    
        // Create a timestamp if incoming event doesn't have one
        this.timeStamp = src && src.timeStamp || jQuery.now();//加時間戳   
    
        // Mark it as fixed
        this[ jQuery.expando ] = true;//jQuery.expando是頁面中每個jQuery副本惟一的標誌。此屬性來判斷當前事件對象是否爲jQuery事件對象
    };
    
    // jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
    // http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
    jQuery.Event.prototype = {
        isDefaultPrevented: returnFalse,//是否已經阻止默認行爲
        isPropagationStopped: returnFalse,//是否已經阻止事件傳播
        isImmediatePropagationStopped: returnFalse,//是否已經阻止事件執行和事件傳播
    
        preventDefault: function() {
            var e = this.originalEvent;
    
            this.isDefaultPrevented = returnTrue;
    
            if ( e && e.preventDefault ) {
                e.preventDefault();
            }
        },
        stopPropagation: function() {
            var e = this.originalEvent;
    
            this.isPropagationStopped = returnTrue;
    
            if ( e && e.stopPropagation ) {
                e.stopPropagation();
            }
        },
        stopImmediatePropagation: function() {
            var e = this.originalEvent;
    
            this.isImmediatePropagationStopped = returnTrue;
    
            if ( e && e.stopImmediatePropagation ) {
                e.stopImmediatePropagation();
            }
    
            this.stopPropagation();
        }
    };

    能夠看到,jQuery用構造函數來建立對象,而且用prototype原型來繼承公有的方法。
    可是jQuery事件對象還沒就此就結束了。由於還須要把像target這些有用的事件屬性從原生的事件對象複製過來。這就是工具方法jQuery.event.fix()的做用了。
    看一下通過fix函數以後Event對象變成了什麼樣子。

    能夠看到,應該有的屬性都有了。這裏的fix還作了一些兼容性的事情。
    在鍵盤事件的時候,只是按下按鍵按鈕的keycode和charcode在不一樣瀏覽器下的表現不同。因此jQuery統一用一個which屬性來指示。
    另外在鼠標事件的時候,由於有個button屬性,該屬性是記錄按下鼠標按鈕的。可是ie和dom標準不同。統一把它修正,並用which來記錄。
    還有一個就ie低版本不支持pageX和pageY的狀況。
    上代碼:jquery

    props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
    
        fixHooks: {},
    
        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: "button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),
            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: function( event ) {//event能夠爲jQuery對象或者原生事件對象   複製事件對象屬性,並修正特殊的
            if ( event[ jQuery.expando ] ) {//判斷是否爲jQuery事件對象
                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 ];//用於存放鍵盤和鼠標事件的不兼容屬性,fixhooks初始值爲空對象
    
            if ( !fixHook ) {//rkeyEvent = /^key/,rmouseEvent = /^(?:mouse|pointer|contextmenu)|click/,
                this.fixHooks[ type ] = fixHook =
                    rmouseEvent.test( type ) ? this.mouseHooks :
                    rkeyEvent.test( type ) ? this.keyHooks :
                    {};
            }
            copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;//存放全部屬性的副本
    
            event = new jQuery.Event( originalEvent );//建立jQuery事件對象
    
            i = copy.length;
            while ( i-- ) {//把原生屬性和修正後的不兼容的屬性複製到jQuery事件對象中
                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 ) {//修正Safari 6.0+, Chrome < 28中event.target爲文本節點的狀況
                event.target = event.target.parentNode;
            }
    
            return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;//修正鼠標事件和鍵盤事件的專屬屬性;鍵盤的按鍵所對應的編碼,和鼠標的clientX、鼠標編碼
        },

     

  3. 接下來是處理函數了。每一個元素只有一個處理函數。能夠說就是dispatch()。爲了配合這個處理函數的工做,還有一個對象,這個對象的每一個元素是一個存放不一樣類型事件處理函數的數組。這個數組中存放着全部的代理事件和自身的事件。先來看看jQuery是如何把處理函數放進這個數組的。jQuery中快捷事件函數click這一些會調用on函數,而on函數又會調用工具函數jQuery.event.add()來綁定事件。因此在jQuery中全部的事件都是經過這個add函數來綁定的。
    下面說說add函數。
    add函數主要是把處理函數處理成一個對象,並把這個對象推入處處理函數對象數組的合適位置。
    這個對象長這個樣子:

    其中,data是咱們自定義的數據。
    handler使咱們傳進去的處理函數
    type是事件類型,
    origType是咱們傳進去的事件類型,
    selector是事件代理的選擇器。
    在這裏爲何會有個type和一個origType呢?這是由於有些事件類型很差控制,因此就會拿別的事件類型來代替和模擬。這些事件有:
    focus/blur由於不支持事件冒泡,因此會用focusein/focusout來代替。

    mouseenter: "mouseover",
    mouseleave: "mouseout",
    pointerenter: "pointerover",
    pointerleave: "pointerout"   在這些中,前面的會用後面的來代替。由於在由父元素進入子元素時重複觸發事件的問題。

    那這些處理函數對象入數組有個什麼樣的順序呢。其實就是先來的在前,後來的在後,代理事件在前。
    看下面例子:api

    <!doctype html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Event fun</title>
    </head>
    <body>
        <div  style="width:200px;height:600px;background-color: red;">
            <div style="width:200px;height:500px;background-color: blue;">
                <div style="width:200px;height:400px;background-color: green;">
                    <div id="a" style="width:200px;height:300px;background-color: black;">
                        <div style="width:200px;height:200px;background-color: yellow;">
                            
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <script src="../../jquery-2.1.1.js"></script>
        <script>
            $(document).on('click',function(){})
            $(document).on('click','#a',function(){})
            $(document).on('click','div',{
                name:'qq',
                age:'dd'
            },function(){
                console.log(1);
            })
            var doc = $(document)
            console.log(document.events.click)
            // document.onclick = function(e){
            //     console.log(1);
            // }
        </script>
        
    </body>
    </html>

    生成的數組對象以下圖。


    上代碼:數組

    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
            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
            if ( !handler.guid ) {//肯定有惟一的id
                handler.guid = jQuery.guid++;
            }
    
            // Init the element's event structure and main handler, if this is the first
            if ( !(events = elemData.events) ) {//若是事件緩存對象不存在,則初始化.用於存儲事件對象
                events = elemData.events = {};
            }
            if ( !(eventHandle = elemData.handle) ) {//取出或初始化主監聽函數
                eventHandle = elemData.handle = function( e ) {//丟棄jQuery.event.trigger()第二個事件和頁面關閉後觸發的事件
                    // Discard the second event of a jQuery.event.trigger() and
                    // when an event is called after a page has unloaded
                    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-- ) {
                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
                type = ( selector ? special.delegateType : special.bindType ) || type;//修正type,若是有selector修正爲代理事件,或者支持更好的類型
    
                // Update special based on newly reset type
                special = jQuery.event.special[ type ] || {};//type可能已經改變,因此嘗試再次獲取修正對象
    
                // handleObj is passed to all event handlers
                handleObj = jQuery.extend({//把監聽函數封裝成監聽對象
                    type: type,//修正後的事件類型
                    origType: origType,//單個原始事件類型
                    data: data,//傳入的附加對象
                    handler: handler,//監聽函數
                    guid: handler.guid,//函數id
                    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
                    if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {//優先使用修正對象的修正方法綁定主監聽函數
                        if ( elem.addEventListener ) {
                            elem.addEventListener( type, eventHandle, false );
                        }
                    }
                }
                    //將監聽對象插入對象數組
                if ( special.add ) {//修正對象有修正方法add,用add
                    special.add.call( elem, handleObj );
    
                    if ( !handleObj.handler.guid ) {
                        handleObj.handler.guid = handler.guid;
                    }
                }
    
                // 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;//記錄綁定過的事件類型
            }
    
        },
    
    //修正事件的代碼。
    // Create mouseenter/leave events using mouseover/out and event-time checks
    // Support: Chrome 15+
    jQuery.each({//修正這四個事件的處理函數
        mouseenter: "mouseover",
        mouseleave: "mouseout",
        pointerenter: "pointerover",
        pointerleave: "pointerout"
    }, function( orig, fix ) {
        jQuery.event.special[ orig ] = {
            delegateType: fix,
            bindType: fix,
    
            handle: function( event ) {
                var ret,
                    target = this,
                    related = event.relatedTarget,
                    handleObj = event.handleObj;
    
                // For mousenter/leave call the handler if related is outside the target.
                // NB: No relatedTarget if the mouse left/entered the browser window
                if ( !related || (related !== target && !jQuery.contains( target, related )) ) {
                    event.type = handleObj.origType;
                    ret = handleObj.handler.apply( this, arguments );
                    event.type = fix;
                }
                return ret;
            }
        };
    });
    
    // Create "bubbling" focus and blur events
    // Support: Firefox, Chrome, Safari
    if ( !support.focusinBubbles ) {//修正focus/blur的處理函數。和特殊的主監聽函數的添加和刪除(由於不支持冒泡)
        jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
    
            // Attach a single capturing handler on the document while someone wants focusin/focusout
            var handler = function( event ) {
                    jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );
                };
    
            jQuery.event.special[ fix ] = {
                setup: function() {
                    var doc = this.ownerDocument || this,
                        attaches = data_priv.access( doc, fix );
    
                    if ( !attaches ) {
                        doc.addEventListener( orig, handler, true );
                    }
                    data_priv.access( doc, fix, ( attaches || 0 ) + 1 );
                },
                teardown: function() {
                    var doc = this.ownerDocument || this,
                        attaches = data_priv.access( doc, fix ) - 1;
    
                    if ( !attaches ) {
                        doc.removeEventListener( orig, handler, true );
                        data_priv.remove( doc, fix );
    
                    } else {
                        data_priv.access( doc, fix, attaches );
                    }
                }
            };
        });
    }
相關文章
相關標籤/搜索