jQuery-1.9.1源码分析系列(十)事件系统之事件体系结构
又是一个重磅功能点。
在分析源码之前分析一下体系结构,有助于源码理解。实际上在jQuery出现之前,DeanEdwards的跨浏览器AddEvent()设计做的已经比较优秀了;而且jQuery事件系统的设计思想也是基于该思想的,所以我们先分析一下DeanEdwards前辈的事件绑定。
a.jQuery事件原型——DeanEdwards的跨浏览器AddEvent()设计
源码解读
//事件添加方法 functionaddEvent(element,type,handler){ //保证每个不同的事件响应函数只有唯一一个id if(!handler.$$guid)handler.$$guid=addEvent.guid++; //给element维护一个events属性,初始化为一个空对象。 //element.events的结构类似于{"click":{...},"dbclick":{...},"change":{...}} if(!element.events)element.events={}; //试图取出element.events中当前事件类型type对应的对象(这个对象更像数组),赋值给handlers //如果element.events中没有当前事件类型type对应的对象则初始化 varhandlers=element.events[type]; if(!handlers){ handlers=element.events[type]={}; //如果这个element已经有了一个对应的事件的响应方法,例如已经有了onclick方法 //就把element的onclick方法赋值给handlers的0元素,此时handlers的结构就是: //{0:function(e){...}},这也是为什么addEvent.guid初始化为1的原因,预留看为0的空间; //此时element.events的结构就是:{"click":{0:function(e){...}},/*省略其他事件类型*/} if(element["on"+type]){ handlers[0]=element["on"+type]; } } //把当前的事件handler存放到handlers中,handler.$$guid=addEvent.guid++;addEvent.guid=1;肯定是从1开始累加的 //因此,这是handlers的结构可能就是{0:function(e){...},1:function(){},2:function(){}等等...} handlers[handler.$$guid]=handler; //下文定义了一个handleEvent(event)函数,将这个函数,绑定到element的type事件上作为事件入口。 //说明:在element进行click时,将会触发handleEvent函数,handleEvent函数将会查找element.events,并调用相应的函数。可以把handleEvent称为“主监听函数” element["on"+type]=handleEvent; }; //计数器 addEvent.guid=1; functionremoveEvent(element,type,handler){ //deletetheeventhandlerfromthehashtable if(element.events&&element.events[type]){ deleteelement.events[type][handler.$$guid]; } }; functionhandleEvent(event){ //兼容ie event=event||window.event; //this是响应事件的节点,这个接点上有events属性(在addEvent中添加的) //获取节点对应事件响应函数列表 varhandlers=this.events[event.type]; //循环响应函数列表执行 for(variinhandlers){ //保持正确的作用域,即this关键字 this.$$handleEvent=handlers[i]; this.$$handleEvent(event); } };
重新梳理一下数据结构,使用一个例子
<inputtype="text"id="chua"onClick="f0();"> functionf0(){...} functionf1(){...} functionf2(){...} functionf3(){...} vardom=document.getElementById("chua"); addEvent(dom,"click",f1); addEvent(dom,"change",f1); addEvent(dom,"change",f2); addEvent(dom,"click",f3); addEvent(dom,"change",f3);
经过addEvent()函数之后,当前的数据结构为:
element:{ onclick:handleEvent(event),//click事件的主监听函数 onchage:handleEvent(event),//change事件的主监听函数 events:{ click:{//这是一个类数组 0:f0,//element已有的事件 1:f1,//下标1实际上就是f1.$$guid 3:f3//下标3实际上就是f3.$$guid,需要注意的是每一个响应事件都有一个唯一的$$guid作为下标 ... }, change:{//这是一个类数组 1:f1, 2:f2, 3:f3 } } }
事件系统会根据调用addEvent的顺序给每个响应函数(也就是addEvent(element,type,handler)中的第三个参数handler)打上标记$$guid。源码
//保证每个不同的事件响应函数只有唯一一个id if(!handler.$$guid)handler.$$guid=addEvent.guid++;
最终三个响应函数的$$guid标记分别是
f1.$$guid=1
f2.$$guid=2
f3.$$guid=3
而根据源码中
handlers[handler.$$guid]=handler;
那么某一个函数在任何事件响应函数集合中的下标位置是固定的。比如click和change事件都调用f3作为响应事件,那么f3在element.events.click以及element.events.change中的下标位置都是f3.$$guid=3;即element.events.click[3]=element.events.change[3]=f3。
这个时候假设又新添了一个事件绑定:addEvent(dom,"focus",f3);那么element.events.focus[3]=f3;这也是对象相比于数组的方便之处,数组不可能没有下标0,1,2就直接有3了,但是对象却可以,此时3是作为对象的一个属性名称。
这样的设计,其实已经具备了jquery事件系统的雏形,包含了几个最主要的特点:
1)element上的所有事件,将保存到element.events属性中,不是直接绑定到element上;这样一个事件可以有无数个响应函数。
2)handleEvent作为element所有事件的“主监听函数”,有它统一管理element上的所有函数。
3)所有浏览器都支持element["on"+type]事件绑定方式,跨浏览器兼容。
好啦,明白了addEvent的事件结构,这个想法确实让人觉得眼前一亮。下面分析jQuery的事件结构
b.jQuery的事件结构
所有的函数添加事件都会进入jQuery.event.add函数。该函数有两个主要功能:添加事件、附加很多事件相关信息。我们直接上源码,源码思想和DeanEdwards的跨浏览器兼容事件添加处理类似。
源码分析
add:function(elem,types,handler,data,selector){ vartmp,events,t,handleObjIn, special,eventHandle,handleObj, handlers,type,namespaces,origType, //获取elem节点对应的缓存数据 elemData=jQuery._data(elem); //没有数据或文本/注释节点不能附加事件(但是允许附加普通对象) if(!elemData){ return; } //调用者能通过自定义数据替换handler if(handler.handler){ handleObjIn=handler; handler=handleObjIn.handler; selector=handleObjIn.selector; } //确保handler函数有唯一的ID,后续会用来查找/删除这个handler函数 if(!handler.guid){ handler.guid=jQuery.guid++; } //如果是初次进入,初始化元素的事件结构和主事件响应入口 if(!(events=elemData.events)){ events=elemData.events={}; } if(!(eventHandle=elemData.handle)){ eventHandle=elemData.handle=function(e){ //当一个事件被调用后页面已经卸载,则放弃jQuery.event.trigger()的第二个事件, returntypeofjQuery!==core_strundefined&&(!e||jQuery.event.triggered!==e.type)? jQuery.event.dispatch.apply(eventHandle.elem,arguments): undefined; }; //将elem作为handle函数的一个特征防止ie非本地事件引起的内存泄露 eventHandle.elem=elem; } //多个事件使用空格隔开的处理 //如jQuery(...).bind("mouseovermouseout",fn); //core_rnotwhite=/\S+/g;匹配空白字符 types=(types||"").match(core_rnotwhite)||[""]; t=types.length; while(t--){ //rtypenamespace=/^([^.]*)(?:\.(.+)|)$/; //获取命名空间和原型事件 tmp=rtypenamespace.exec(types[t])||[]; type=origType=tmp[1]; namespaces=(tmp[2]||"").split(".").sort(); //如果事件改变其类型,使用special事件处理器来处理更改后的事件类型 special=jQuery.event.special[type]||{}; //如果选择器已定义,确定special事件API类型,否则给他一个类型 type=(selector?special.delegateType:special.bindType)||type; //基于新设置的类型更新special special=jQuery.event.special[type]||{}; //handleObj贯穿整个事件处理 handleObj=jQuery.extend({ type:type, origType:origType, data:data, handler:handler, guid:handler.guid, selector:selector, //Foruseinlibrariesimplementing.is().WeusethisforPOSmatchingin`select` //"needsContext":newRegExp("^"+whitespace+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ //whitespace+"*((?:-\\d)?\\d*)"+whitespace+"*\\)|)(?=[^-]|$)","i") //用来判断亲密关系 needsContext:selector&&jQuery.expr.match.needsContext.test(selector), namespace:namespaces.join(".") },handleObjIn); //初次使用时初始化事件处理器队列 if(!(handlers=events[type])){ handlers=events[type]=[]; handlers.delegateCount=0; //非自定义事件,如果special事件处理器返回false,则只能使用addEventListener/attachEvent if(!special.setup||special.setup.call(elem,data,namespaces,eventHandle)===false){ //给元素绑定全局事件 if(elem.addEventListener){ elem.addEventListener(type,eventHandle,false); }elseif(elem.attachEvent){ elem.attachEvent("on"+type,eventHandle); } } } //自定义事件绑定 if(special.add){ special.add.call(elem,handleObj); if(!handleObj.handler.guid){ handleObj.handler.guid=handler.guid; } } //将事件对象handleObj添加到元素的处理列表,代理计数递增 if(selector){ handlers.splice(handlers.delegateCount++,0,handleObj); }else{ handlers.push(handleObj); } //跟踪那个事件曾经被使用过,用于事件优化 jQuery.event.global[type]=true; } //防止ie内存泄漏 elem=null; }
依然用实例来说明jQuery的事件结构
<divid="#center"></div> <script> functiondohander(){console.log("dohander")}; functiondot(){console.log("dot");} $(document).on("click",'#center',dohander) .on("click",'#center',dot) .on("click",dot); </script>
经过添加处理环节,事件添加到了元素上,而且节点对应的缓存数据也添加了相应的数据。结构如下
elemData=jQuery._data(elem); elemData={ events:{ click:{//Array[3] 0:{ data:undefined/{...}, guid:2,//处理函数的id handler:functiondohander(){…}, namespace:"", needsContext:false, origType:"click", selector:"#center",//选择器,用来区分不同事件源 type:"click" } 1:{ data:undefined/{...}, guid:3, handler:functiondot(){…}, namespace:"", needsContext:false, origType:"click", selector:"#center", type:"click" } 2:{ data:undefined, guid:3, handler:functiondot(){…}, namespace:"", needsContext:false, origType:"click", selector:undefined, type:"click" } delegateCount:2,//委托事件数量,有selector的才是委托事件 length:3 } } handle:function(e){…}/*事件处理主入口*/{ elem:document//属于handle对象的特征 } }
jQuery的处理和DeanEdwards的跨浏览器兼容事件添加处理类似,比如为每一个函数添加guid;使用events对象存放响应事件列表,有一个总的事件处理入口handle等。
jQuery做了哪些改进?
1)事件数据不再直接保存在节点上,而是使用jQuery缓存系统内(内部使用的缓存jQuery._data方式存取)
2)事件委托:绑定到当前节点(例子中当前节点是document根节点)的处理函数不仅仅包含当前节点触发事件(click)响应时处理的事件(例子中selector为undefined时对应的处理函数dot);还代理了其他节点(例子中的#center节点)触发事件(click)响应时处理的事件(例子中selector为"#center"对应的处理事件doHandler和dot);委托机制在后续分析。
3)增加了很多功能数据,比如命名空间namespace:这个主要用在自定义事件自定义触发,比如$(document).on("chua.click",'#center',dot),主动触发$("#center").trigger("chua.click")。还有额外数据data:虽然没有看到那个地方有被用到。
到此jQuery的事件结构就清楚了。后面再分析事件的绑定和触发以及委托原理。