jQuery源碼解讀-事件分析


最原始的事件注冊

    addEventListener方法大家應該都很熟悉,它是Html元素注冊事件最原始的方法。先看下addEventListener方法簽名:

element.addEventListener(event, function, useCapture)

    event:事件名,例如“click”,這里要提醒的一點是不要加前綴“on”;
    function:事件觸發時執行的函數;
    userCapture:默認為false,表示event事件在冒泡階段觸發。如果設置為true,則事件將會在捕獲階段觸發。如果不清楚什么是捕獲和冒泡,請自覺了解事件的冒泡機制(友情鏈接:勤能補挫-簡單But易錯的JS&CSS問題總結)。
    雖然addEventListener包含了三個參數,但一般我們都只使用了前兩個參數,下面的代碼只使用了兩個參數:

document.getElementById("myBtn").addEventListener("click", function() {
    alert(“我是在冒泡階段觸發的哦!”);
});  

    上面代碼注冊的函數會在冒泡階段觸發,如果想在捕獲階段觸發,直接把第三個參數傳遞進去就ok了。在實現DOM元素拖拽功能時,會使用到捕獲方式。

    另外,IE8以及之前的版本不支持事件按捕獲形式傳播,並且注冊方法也沒有addEventListener函數,IE為事件注冊提供了attachEvent方法。和addEventListener相似,也包含有event和function參數,但不包含第三個參數。

jQuery事件注冊

     jQuery的事件函數通過jQuery.fn.extend附加到jQuery對象,jQuery.fn.extend包含了jQuery的所有事件注冊函數。那么jQuery到底提供了哪些事件函數?這里把這些函數分層了三類:

    (1)和事件同名的函數:jQuery幾乎提供了所有DOM元素事件的同名函數,像我們經常使用的click、focus、scroll等函數。使用也很簡單,例如我們要給div元素綁定click事件,可以直接寫成$(“div”).click(function(){})。DOM元素的事件有很多,jQuery為每個事件都添加了同名的注冊函數嗎?看源碼!

//循環遍歷所有的dom元素事件名數組
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 ) {
    //把dom元素所有事件通過fn[事件名]的方式添加到jquery對象
    // Handle event binding
    jQuery.fn[ name ] = function( data, fn ) {
        //如果參數長度大於0,則調用on方法委托函數到name事件;如果參數長為0,則觸發事件執行
        return arguments.length > 0 ?
        this.on( name, null, data, fn ) :
        this.trigger( name );
    };
});

    首先看到的是一串包含了所有DOM元素事件的字符串,通過空格把字符串分隔成數組。如果傳遞的參數長度大於0,則調用jQuery對象的on方法注冊事件。如果參數長度為0,則直接調用trigger方法觸發事件。例如(“div”).click(function())將會調用on方法注冊事件,而(“div”).click()則調用trigger方法,立即觸發click事件。
    上面的代碼有幾點需要作下解釋:
    jQuery.fn中的函數包含的上下文this是指向jQuery實體,例如$(“div”)實體。
    jQuery.fn[name] = function(){}等效於jQuery.fn.name = function(){},例如jQuery.fn[“click”] = function(){}等效於jQuery.fn.click = function(){}。
    This.on和this.trigger方法這里暫不忙解釋。

    (2)綁定和委托函數:bind/unbind和delegate/undelegate方法通過jQuery.fn.extend附加到jQuery對象上。代碼很簡單:

jQuery.fn.extend({
//事件綁定
bind: function( types, data, fn ) {
    return this.on( types, null, data, fn );
},
//事件解綁
unbind: function( types, fn ) {
    return this.off( types, null, fn );
},
//事件委托
delegate: function( selector, types, data, fn ) {
    return this.on( types, selector, data, fn );
},
//委托解綁
undelegate: function( selector, types, fn ) {
    return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
}
});

    bind和delegate都是直接調用jQuery對象的on函數,唯一區別是傳遞的參數不同,bind的第二個參數為null,而委托的第二個參數是一個selector。別小看這個區別,使用jQuery綁定事件常出的問題部分原因就是沒搞清楚這兩個參數的區別。

    (3)底層注冊函數:前面介紹的和事件同名的函數、綁定和委托函數最終都是調用了jQuery對象的on函數,我們在編程的時候也可以直接使用on函數。on函數代碼比較復雜,我們先看看外殼:

jQuery.fn.extend({
    //比較底層的事件委托函數,其他函數都是調用這個來和元素建立綁定或者委托
    on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
        return this.each( function() {
            jQuery.event.add( this, types, fn, data, selector );
        });
    },
    //一次性事件綁定
    one: function( types, selector, data, fn ) {
        return this.on( types, selector, data, fn, 1 );
    },
    //比較底層的事件解綁,其他解綁函數都是調用該函數執行解綁
    off: function( types, selector, fn ) {
        return this.each(function() {
            jQuery.event.remove( this, types, fn, selector );
        });
    },
    //觸發事件
    trigger: function( type, data ) {
        return this.each(function() {
            jQuery.event.trigger( type, data, this );
        });
    },
    //只執行元素綁定的處理函數,不會觸發瀏覽器的默認動作
    triggerHandler: function( type, data ) {
        var elem = this[0];
        if ( elem ) {
            return jQuery.event.trigger( type, data, elem, true );
        }
    }
});

    為什么說是底層的函數?因為前面的所有綁定最終都是調用on函數,所有的解綁最終調用off函數。這里還包含了trigger和triggerHandler函數,前一個是觸發元素的所有type事件行為,而triggerHandler只觸發綁定的函數而不觸發行為。例如focus事件,triggerHandler只會觸發綁定給元素的focus處理函數,而不會真的讓元素獲得焦點。但trigger函數會讓元素獲取焦點。

    匯總一下,jQuery提供的事件處理函數不外乎也就下面這些。

image

委托還是綁定?

    這里為什么提出了委托和綁定?事出有因,我們慢慢來分析。之前介紹了幾類事件綁定,先分下類便於后面的分析。以什么分類?就以調用on函數的第二個參數為不為null。

(1)為null的一類on(types, null, data, fn)事件

    bind、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。

(2)不為null的一類on(types, selector, data, fn)事件

    delegate、on

    接下來我們舉一個場景:給div容器(class為parent)列表中的每一項(class為child)添加click事件,並且列表的項可動態添加。

<div class="parent">
<div class="child">第1個兒子</div>
<div class="child">第2個兒子</div>
<div class="child">第3個兒子</div>
</div>
<button id="btn">生兒子</button>
<script type="text/javascript">
    var i = 4;
    (".parent.child").click(function(){alert("我是你兒子"});
    //(".parent.child").click(function(){alert("我是你兒子"});
    //(".parent .child").bind("click", function(){
        // alert("我是你兒子");
    // })
    ("#btn").click(function(){             
        $(".parent").append("<div class='child'>第" + (i++) + "個兒子</div>");         
    });    
 </script>

    頁面加載后點擊前三個兒子都會提示“我是你兒子”,現在我點擊btn按鈕,添加第四個兒子,然后再點擊新增項看看。發現沒有再彈出提示信息。上面代碼注冊事件使用的是click或者bind函數,效果都是一樣:動態添加的子項沒有觸發事件了。其實,“為null的一類”事件效果都是這樣。現在我們再把事件綁定改成delegate或者on函數:

//(".parent").on("click", ".child", function(){
    // alert("我是你兒子");
// });
$(".parent").delegate(".child", "click", function(){
    alert("我是你兒子");
});

    測試結果發現,不管是on或者delegate,我們后面動態添加的子項都能觸發事件。

    通過上面的場景不難看出,click和bind函數只支持靜態綁定,只能綁定給已經有的節點,后期動態生成的節點不支持。這樣的行為我們可稱為“綁定”。而通過delegate或者on方法通過傳遞一個selector,把通過selector篩選的元素的事件全權“委托”給父容器。所以事件其實是綁定在父容器上,只是在處理事件時jQuery內部做了委托處理。
    那么,到底是委托好還是綁定好?個人建議如果篩選的元素比較少,可以使用click或者bind,比較簡單並且代碼也容易理解。但如果篩選出的元素可能包含成百上千,那么肯定使用delegate或者on,這樣性能比bind高多了。delegate、on事件只會綁定給父容器,即使1000個節點,還是只綁定一次。而bind的話就得乖乖的綁定1000次。
不管是委托還是綁定,都是通過on注冊。所以搞清楚on函數的實現也就搞清楚了jQuery的事件機制。

jQuery源代碼分析

    jQuery.fn.on函數

    既然綁定和委托最終都是調用on函數,那么只要把on方法代碼流程了解清楚,整個事件綁定機制也了解的差不多。On函數代碼其實比較簡單,包含參數處理和事件添加兩個部分。函數包含了5個參數:

on: function( types, selector, data, fn, /*INTERNAL*/ one )

    但是我們經常使用on函數並沒有傳遞這么多參數,而是像這樣:

(“a”).on(“click”,function());(“a”).on(“click”,function());(“a”).on(“click”, “p”, function(){});
(“a”).on(“click,mouseover,focus”,function());
(“a”).on(“click,mouseover,focus”,function());
(“”).on(“click”, {id: 1, name: “test”}, function{});

    on函數大部分代碼都是處理傳入的參數,最后三行代碼使用each遍歷jQuery對象中的元素並調用jQuery.event.add方法。源代碼如下:

<DIV class=cnblogs_code 
style="BORDER-TOP: #cccccc 1px solid; BORDER-RIGHT: #cccccc 1px solid; BORDER-BOTTOM: #cccccc 1px solid; PADDING-BOTTOM: 5px; PADDING-TOP: 5px; PADDING-LEFT: 5px; BORDER-LEFT: #cccccc 1px solid; PADDING-RIGHT: 5px; BACKGROUND-COLOR: #f5f5f5"><PRE><SPAN style="COLOR: #000000">jQuery.fn.extend({
    //比較底層的事件委托函數,其他函數都是調用這個來和元素建立綁定或者委托
    on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
        var origFn, type;
        //參數為types/handlers,("click", function)
        if ( typeof types === "object" ) {
          // ( types-Object, selector, data )。例如({'click': function1,'focus': function2}, selector, data)
            if ( typeof selector !== "string" ) {
                // ( types-Object, data )。例如({'click': function1,'focus': function2}, data)
                data = data || selector;
                selector = undefined;
            }
            //遍歷{'click': function1,'focus': function2}
            for ( type in types ) {
            //每個type再單獨調用on注冊一次
            this.on( type, selector, data, types[ type ], one );
            }
            return this;
         }
        //只有兩個參數,{types,fn}
        if ( data == null &amp;&amp; fn == null ) {
            // ( types, fn )
            fn = selector;
            data = selector = undefined;
        }
        //fn == null &amp;&amp; data != null,只有三個參數的情況
        else if ( fn == null ) {
            if ( typeof selector === "string" ) {
                // ( types, selector, fn ),例如:("click", "a,p", function(){})
                fn = data;
                data = undefined;
            } else {
                // ( types, data, fn ), 例如:("click", {id: 1, name: "test"}, function(e){})
                fn = data;
                data = selector;
                selector = undefined;
            }
        }
        if ( fn === false ) { //如果fn等於false,重新賦給fn一個return false的函數。
            fn = returnFalse;
        } else if ( !fn ) { //如果fn未定義或者為null,不做任何操作,直接返回鏈式對象this
            return this;
        }

        if ( one === 1 ) { //事件只執行一次
            origFn = fn;
            fn = function( event ) { //重寫fn函數,在執行fn函數一次后,注銷事件
                // Can use an empty set, since event contains the info
                jQuery().off( event );
                return origFn.apply( this, arguments );
             };
             // Use same guid so caller can remove using origFn
             fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); //賦值fn.guid等於原始函數origFn.guid
        }
        //jQuery對象包含的元素是一個集合,所以需要遍歷每個元素執行event.add
        return this.each( function() {
            //event.add做了什么操作?
            jQuery.event.add( this, types, fn, data, selector );
            });
        }
    }

    jQuery.event對象  

    jQuery.fn.on函數最后三行代碼調用了jQuery.event.add函數,add是jQuery.event的一個函數。在了解add之前先看看jQuery.event,jQuery.event究竟包含哪些東西:

jQuery.event = {
    //函數,為元素添加事件
    add: function( elem, types, handler, data, selector ) {},
    //函數,為元素刪除事件
    remove: function( elem, types, handler, selector, mappedTypes ) {},
    //函數,觸發元素事件
    trigger: function( event, data, elem, onlyHandlers ) {},
    //函數,執行元素事件
    dispatch: function( event ) {},
    //函數,事件隊列
    handlers: function( event, handlers ) {},
    //屬性,KeyEvent和MouseEvent事件屬性
    props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
    //函數,擴展event。添加一些附加屬性,像target、type、origainEvent等屬性
    fix: function( event ) {},
    //對象,特殊事件
    special: {},
    //函數,模擬事件行為,例如focus、unfocus行為
    simulate: function( type, elem, event, bubble ) {}
}

    現在我們想要搞清楚的是jQuery怎樣添加事件,以及如何執行事件。要了解清楚這些問題,就必須得搞清楚代碼中的add、dispatch、handlers三個函數。

    為了容易理解這些函數的關系,下面是一個函數執行順序的流程圖:

image

    jQuery.event.add函數

    事件是建立在DOM元素之上,DOM元素和事件要建立關系,最原始的方法是在DOM元素上綁定事件。jQuery為了不破壞DOM樹結構,通過緩存的方式保存事件。jQuery內部有一個叫做Data的緩存對象,通過key/value這種方式緩存數據。細心的同學在使用jQuery時會發現DOM元素多了一個以jQuery開頭的屬性,例如jQuery20303812802915245450.4513941336609537:3。這個屬性正是jQuery緩存的key值。
    Add函數中的elemData就是一個類型為Data的緩存對象,在調用get時需要把元素作為參數傳遞進去, 查找元素的屬性以jQuery開始的元素句柄。例如elem[‘jQuery203038128.l..537’]這種形式。elemData需要關注另外兩個屬性:handle和events。
    handler就是一個調用了dispatch的匿名函數,events是一個數組,每一項是一個handleObj對象,包含type、origType、data、handler、guid、selector等屬性。如果傳遞的types為”click focus mouseenter”,那么events數組就包含了三個handleObj對象。
另外還得調用addEventListener給委托元素注冊事件,不然事件觸發不了。

    總得來說,add函數干了幾件事:

    如果沒有為委托元素elem建立緩存,在調用get時創建緩存;
    賦予elemData.handle一個匿名函數,調用event.dispatch函數。
    往elemData.events數組添加不同事件類型的事件對象handleObj。
    給elem綁定一個types類型的事件,觸發時調用elemData.handle。

add: function( elem, types, handler, data, selector ) {
    var handleObjIn, eventHandle, tmp,
    events, t, handleObj,
    special, handlers, type, namespaces, origType,
    elemData = data_priv.get( elem ); //存儲事件句柄對象,elem元素的句柄對象
    
    if ( !handler.guid ) {
          handler.guid = jQuery.guid++; //創建編號,為每一個事件句柄給一個標示
    }
    
    if ( !(events = elemData.events) ) {
         events = elemData.events = {}; //events是jQuery內部維護的事件列隊
    }
    if ( !(eventHandle = elemData.handle) ) { //handle是實際綁定到elem中的事件處理函數
        eventHandle = elemData.handle = function( e ) {
        jQuery.event.dispatch.apply( eventHandle.elem, arguments );
    };
    eventHandle.elem = elem;
    //事件可能是通過空格鍵分隔的字符串,所以將其變成字符串數組
    types = ( types || "" ).match( core_rnotwhite ) || [""];
    t = types.length;
    while ( t-- ) {
        // 這里把handleObj叫做事件處理對象,擴展一些來着handleObjIn的屬性
        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 );
        
        // 初始化事件處理列隊,如果是第一次使用,將執行語句
        if ( !(handlers = events[ type ]) ) {
            handlers = events[ type ] = [];
            handlers.delegateCount = 0;
            
            if ( elem.addEventListener ) {
                 elem.addEventListener( type, eventHandle, false );
            }
        }
    
        // 將事件處理對象推入處理列表,姑且定義為事件處理對象包
        if ( selector ) {
            handlers.splice( handlers.delegateCount++, 0, handleObj );
        } else {
            handlers.push( handleObj );
        }
        // 表示事件曾經使用過,用於事件優化
        jQuery.event.global[ type ] = true;
    }
    // 設置為null避免IE中循環引用導致的內存泄露
    elem = null;
}

    jQuery.event.dispatch函數

    委托元素觸發事件時會調用dispatch函數,dispatch函數需要做的就是執行我們添加的handler函數。

    jQuery事件中的event和原生event是有區別的,做了擴展。所以代碼中重新生成了一個可寫的event:jQuery.event.fix(event)。包含的屬性:

    delegateTarget、currentTarget、handleObj、data、preventDefault、stopPropagation。

    由於我們添加的事件函數之前保存到了緩存中,所以調用data_priv.get取出緩存。
    代碼生成了一個handlerQueue隊列,這里先不忙介紹jQuery.event.handlers函數。handlerQueue是一個數組,每一項是一個格式為{ elem: cur, handlers: matches }的對象。cur是DOM元素,handlers是處理函數數組。

    兩個while循環:

   第一個循環遍歷handlerQueue,item為{ elem: cur, handlers: matches }。
    第二個循環遍歷handlers,分別執行每一個handler。

    event做了封裝,我們可以在事件函數中通過event.data獲取額外的信息。
    dispatch函數有判斷處理函數的返回結果,如果返回結果等於false,阻止冒泡。調用preventDefault、stopPropagation終止后續事件的繼續傳遞。

dispatch: function( event ) {
    //把event生成一個可寫的對象
    event = jQuery.event.fix( event );
    
    var handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [];
    event.delegateTarget = this;
    handlerQueue = jQuery.event.handlers.call( this, event, handlers );
    
    i = 0;
    while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
        event.currentTarget = matched.elem;
        
        j = 0;
        while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {
            if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {
                event.handleObj = handleObj;
                event.data = handleObj.data;
                ret = handleObj.handler.apply( matched.elem, args );
                if ( ret !== undefined ) {
                    if ( (event.result = ret) === false ) {
                        event.preventDefault();
                        event.stopPropagation();
                    }
                }
            }
        }
    }
    
    return event.result;
}

    jQuery.event.handler函數

    dispatch函數有調用handler函數生成一個handler隊列,其實整個事件流程中最能體現委托的地方就是handler函數。
    這里有兩個端點,cur = event.target(事件觸發元素)和this(事件委托元素)。jQuery從cur通過parentNode 一層層往上遍歷,通過selector匹配當前元素。
    每一個cur元素都會遍歷一次handlers。handlers的項是一個handleObj對象,包含selector屬性。通過jQuery( sel, this ).index( cur )判斷當前元素是否匹配,匹配成功就加到matches數組。
    handlers遍歷完后,如果matches數組有值,就把當前元素cur和matches作為一個對象附加到handlerQueue中。
一個委托元素可能包含委托和普通事件(直接綁定的事件),目前我們只根據delegateCount遍歷了委托事件,所以最后還得通過handlers.slice( delegateCount )把后面的普通事件添加到隊列中。

    什么是委托事件和普通事件?

    (“div”).on(“click”,“a,p”,function)這種形式添加的function是div的委托事件;而像(“div”).on(“click”, function)形式添加的事件就是div元素的一個普通事件。handlers數組中delegateCount之前的都是委托事件,之后的是普通事件。

handlers: function( event, handlers ) {
    var handlerQueue = [],
    delegateCount = handlers.delegateCount,
    cur = event.target;
    //向上遍歷DOM元素
    for ( ; cur !== this; cur = cur.parentNode || this ) {
        if ( cur.disabled !== true || event.type !== "click" ) {
            matches = [];
            for ( i = 0; i < delegateCount; i++ ) {
                handleObj = handlers[ i ];
                //獲取handler的selector
                sel = handleObj.selector + " ";
                
                if ( matches[ sel ] === undefined ) {
                    matches[ sel ] = handleObj.needsContext ?
                    //查看通過selector篩選的元素是否包含cur
                    jQuery( sel, this ).index( cur ) >= 0 :
                    jQuery.find( sel, this, null, [ cur ] ).length;
                }
                //如果元素匹配成功,則把handleObj添加到matches數組。
                if ( matches[ sel ] ) {
                    matches.push( handleObj );
                }
            }
            //如果matches數組長度大於0,附加cur和matches到隊列中
            if ( matches.length ) {
                handlerQueue.push({ elem: cur, handlers: matches });
            }
        }
    }
    
    if ( delegateCount < handlers.length ) {
        //表示還有為委托事件函數,也要附加到隊列中
        handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });
    }
    
    return handlerQueue;
}

   如果本篇內容對大家有幫助,請點擊頁面右下角的關注。如果覺得不好,也歡迎拍磚。你們的評價就是博主的動力!下篇內容,敬請期待!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM