jQuery-1.9.1源码分析系列(十)事件系统——事件体系结构

王朝学院·作者佚名  2016-08-27
窄屏简体版  字體: 小  |  中  |  大  |  超大  

又是一个重磅功能点。

在分析源码之前分析一下体系结构,有助于源码理解。实际上在jQuery出现之前,Dean Edwards的跨浏览器AddEvent()设计做的已经比较优秀了;而且jQuery事件系统的设计思想也是基于该思想的,所以我们先分析一下Dean Edwards前辈的事件绑定。

a. jQuery事件原型——Dean Edwards的跨浏览器AddEvent()设计源码解读

//事件添加方法functionaddEvent(element, type, handler) {//保证每个不同的事件响应函数只有唯一一个idif(!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) {//delete the event handler from the hash tableif(element.events &&element.events[type]) {deleteelement.events[type][handler.$$guid];

}

};functionhandleEvent(event) {//兼容ieevent = event ||window.event;//this是响应事件的节点,这个接点上有events属性(在addEvent中添加的)//获取节点对应事件响应函数列表varhandlers =this.events[event.type];//循环响应函数列表执行for(variinhandlers) {//保持正确的作用域,即this关键字this.$$handleEvent =handlers[i];this.$$handleEvent(event);

}

};

View Code

重新梳理一下数据结构,使用一个例子

<input type="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.$$guid3:f3//下标3实际上就是f3.$$guid,需要注意的是每一个响应事件都有一个唯一的$$guid作为下标...},

change:{//这是一个类数组1: f1,

2: f2,

3: f3

}}

}

事件系统会根据调用addEvent的顺序给每个响应函数(也就是addEvent(element, type, handler)中的第三个参数handler)打上标记$$guid。源码

//保证每个不同的事件响应函数只有唯一一个idif(!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函数。该函数有两个主要功能:添加事件、附加很多事件相关信息。我们直接上源码,源码思想和Dean Edwards的跨浏览器兼容事件添加处理类似。

源码分析

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;

}//调用者能通过自定义数据替换handlerif( 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("mouSEOver mouseout", 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;//基于新设置的类型更新specialspecial = jQuery.event.special[ type ] ||{};//handleObj贯穿整个事件处理handleObj =jQuery.extend({

type: type,

origType: origType,

data: data,

handler: handler,

guid: handler.guid,

selector: selector,//For use in libraries implementing .is(). We use this for POS matching in `select`//"needsContext": new RegExp( "^" + 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/attachEventif( !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;

}

View Code

依然用实例来说明jQuery的事件结构

<div id="#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,//处理函数的idhandler: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的处理和Dean Edwards的跨浏览器兼容事件添加处理类似,比如为每一个函数添加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的事件结构就清楚了。后面再分析事件的绑定和触发以及委托原理。

如果觉得本文不错,请点击右下方【推荐】!

 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
 
© 2005- 王朝網路 版權所有 導航