最原始的事件注册
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提供的事件处理函数不外乎也就下面这些。
委托还是绑定?
这里为什么提出了委托和绑定?事出有因,我们慢慢来分析。之前介绍了几类事件绑定,先分下类便于后面的分析。以什么分类?就以调用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 && fn == null ) { // ( types, fn ) fn = selector; data = selector = undefined; } //fn == null && 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.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; }
如果本篇内容对大家有帮助,请点击页面右下角的关注。如果觉得不好,也欢迎拍砖。你们的评价就是博主的动力!下篇内容,敬请期待!