jQuery源码解读-事件分析

时间:2023-11-23 22:03:14

最原始的事件注册

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提供的事件处理函数不外乎也就下面这些。

jQuery源码解读-事件分析

委托还是绑定?

这里为什么提出了委托和绑定?事出有因,我们慢慢来分析。之前介绍了几类事件绑定,先分下类便于后面的分析。以什么分类?就以调用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三个函数。

为了容易理解这些函数的关系,下面是一个函数执行顺序的流程图:

jQuery源码解读-事件分析

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

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