又是一个重磅功能点。
在分析源码之前分析一下体系结构,有助于源码理解。实际上在jQuery出现之前,做的已经比较优秀了;而且jQuery事件系统的设计思想也是基于该思想的,所以我们先分析一下Dean Edwards前辈的事件绑定。
a. jQuery事件原型——Dean Edwards的跨浏览器AddEvent()设计
源码解读
添加
方法
function addEvent(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对应的对象则初始化
var handlers = 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;
function removeEvent(element,handler) {
// delete the event handler from the hash table
if (element.events && element.events[type]) {
delete element.events[type][handler.$$guid];
}
};
function handleEvent(event) {
//兼容ie
event = event || window.event;
//this是响应事件的节点,这个接点上有events
属性(在addEvent中
添加的)
//
获取节点对应事件响应
函数列表
var handlers = this.events[event.type];
// 循环响应
函数列表执行
for (var i in handlers) {
//保持正确的作用域,即this关键字
this.$$handleEvent = handlers[i];
this.$$handleEvent(event);
}
};
重新梳理一下数据结构,使用一个例子
经过addEvent()函数之后,当前的数据结构为:
函数
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,handler)中的第三个参数handler)打上标记$$guid。源码
函数只有唯一一个id
if (!handler.$$guid) handler.$$guid = addEvent.guid++;
最终三个响应函数的$$guid标记分别是
f1.$$guid = 1
f2.$$guid = 2
f3.$$guid = 3
而根据源码中
那么某一个函数在任何事件响应函数集合中的下标位置是固定的。比如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的跨浏览器兼容事件添加处理类似。
源码分析
+~]|:(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,eventHandle ) === false ) {
//给元素绑定全局事件
if ( elem.addEventListener ) {
elem.addEventListener( type,false );
} else if ( 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++,handleObj );
} else {
handlers.push( handleObj );
}
//跟踪那个事件曾经被使用过,用于事件优化
jQuery.event.global[ type ] = true;
}
//防止ie内存泄漏
elem = null;
}
依然用实例来说明jQuery的事件结构
经过添加处理环节,事件添加到了元素上,而且节点对应的缓存数据也添加了相应的数据。结构如下
函数的id
handler: function dohander(){…}, namespace: "", needsContext: false, origType: "click", selector: "#center",//选择器,用来区分不同事件源
type: "click"
}
1: {
data: undefined/{...}, guid: 3, handler: function dot(){…}, type: "click"
}
2: {
data: undefined, 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",dot),主动触发$("#center").trigger("chua.click")。还有额外数据data:虽然没有看到那个地方有被用到。
到此jQuery的事件结构就清楚了。后面再分析事件的绑定和触发以及委托原理。