jQuery源码解读-事件分析

故事扮演 提交于 2019-11-29 05:12:31

最原始的事件注册

    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;
}

   如果本篇内容对大家有帮助,请点击页面右下角的关注。如果觉得不好,也欢迎拍砖。你们的评价就是博主的动力!下篇内容,敬请期待!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!