jQuery源码学习笔记九

时间:2021-12-15 09:00:51

最近几天搞了一个基于事件代理的事件系统,但即便是事件代理还是要依赖于事件注册,因此深入研究了jQuery的事件系统,整理出来分享一下。

由于IE与标准浏览器闹别扭,我们通过虽然弄一个叫addEvent的函数来屏蔽差异。以下就是一个经典的addEvent函数:


var addEvent = function( obj, type, fn ) {
if (obj.addEventListener)
obj.addEventListener( type, fn, false );
else if (obj.attachEvent) {
obj["e"+type+fn] = fn;
obj.attachEvent( "on"+type, function() {
obj["e"+type+fn]();
} );
}
};

但这简洁函数有许多缺点,如不能处理IE下绑定的回调函数的执行顺序问题,也根本无法消除事件对象的差异。于是有

 
//http://dean.edwards.name/weblog/2005/10/add-event/
//http://dean.edwards.name/weblog/2005/10/add-event2/
function addEvent(element, type, handler) {
// assign each event handler a unique ID
//在每个回调函数上绑定了一个UUID
if (!handler.$$guid) handler.$$guid = addEvent.guid++;
// create a hash table of event types for the element
//在要绑定事件的元素节点上设置一个特殊的属性,用来储存事件
if (!element.events) element.events = {};
// create a hash table of event handlers for each element/event pair
//evets函数的键名为事件的类型名,或者说把事件按类型来按理
var handlers = element.events[type];
if (!handlers) {
handlers = element.events[type] = {};
// store the existing event handler (if there is one)
if (element["on" + type]) {
handlers[0] = element["on" + type];//DOM1.0
}
}
// store the event handler in the hash table
//让一个类型对应多个回调函数
handlers[handler.$$guid] = handler;
// assign a global event handler to do all the work
element["on" + type] = handleEvent;
};
// a counter used to create unique IDs
addEvent.guid = 1;

function removeEvent(element, type, handler) {
// delete the event handler from the hash table
if (element.events && element.events[type]) {
//移除当前类型对应的某个回调函数
delete element.events[type][handler.$$guid];
}
};

function handleEvent(event) {
var returnValue = true;
// grab the event object (IE uses a global event object)
event = event || fixEvent(window.event);
// get a reference to the hash table of event handlers
var handlers = this.events[event.type];
// execute each event handler
for (var i in handlers) {
this.$$handleEvent = handlers[i];
if (this.$$handleEvent(event) === false) {
returnValue = false;
}
}
return returnValue;
};

function fixEvent(event) {
// add W3C standard event methods
event.preventDefault = fixEvent.preventDefault;
event.stopPropagation = fixEvent.stopPropagation;
return event;
};
fixEvent.preventDefault = function() {
this.returnValue = false;
};
fixEvent.stopPropagation = function() {
this.cancelBubble = true;
};

完美的解决了IE的执行顺序问题,并为IE的事件对象添加了两个方法preventDefault与stopPropagation,jQuery的事件系统就是基于它发展而来。下面jQuery1.0.1的代码:

 
event: {
// Bind an event to an element
// Original by Dean Edwards
add: function (element, type, handler) {
// For whatever reason, IE has trouble passing the window object
// around, causing it to be cloned in the process
if (jQuery.browser.msie && element.setInterval != undefined) element = window;
// Make sure that the function being executed has a unique ID
if (!handler.guid) handler.guid = this.guid++;
// Init the element's event structure
if (!element.events) element.events = {};
// Get the current list of functions bound to this event
var handlers = element.events[type];
// If it hasn't been initialized yet
if (!handlers) {
// Init the event handler queue
handlers = element.events[type] = {};
// Remember an existing handler, if it's already there
if (element["on" + type]) handlers[0] = element["on" + type];
}

// Add the function to the element's handler list
handlers[handler.guid] = handler;

// And bind the global event handler to the element
element["on" + type] = this.handle;
//上面基本和DC大神的一致
// Remember the function in a global list (for triggering)
if (!this.global[type]) this.global[type] = [];
this.global[type].push(element);
},

guid: 1,
global: {},

// Detach an event or set of events from an element
remove: function (element, type, handler) {
if (element.events) if (type && element.events[type]) if (handler) delete element.events[type][handler.guid];
else for (var i in element.events[type]) delete element.events[type][i];
else for (var j in element.events) this.remove(element, j);
},
//触发,为什么不叫fire呢?!
trigger: function (type, data, element) {
// Touch up the incoming data
data = data || [];

// Handle a global trigger
if (!element) {
var g = this.global[type];
if (g) for (var i = 0; i < g.length; i++) this.trigger(type, data, g[i]);
// Handle triggering a single element
} else if (element["on" + type]) {
// Pass along a fake event
data.unshift(this.fix({
type: type,
target: element
}));
// Trigger the event
element["on" + type].apply(element, data);
}
},

handle: function (event) {
if (typeof jQuery == "undefined") return;
event = event || jQuery.event.fix(window.event);
// If no correct event was found, fail
if (!event) return;
var returnValue = true;
var c = this.events[event.type];
for (var j in c) {
if (c[j].apply(this, [event]) === false) {
event.preventDefault();
event.stopPropagation();
returnValue = false;
}
}
return returnValue;
},

fix: function (event) {
if (event) {
event.preventDefault = function () {
this.returnValue = false;
};
event.stopPropagation = function () {
this.cancelBubble = true;
};
}
return event;
}
}

我们来看一个这个经典的基于事件注册的事件系统,几年前主流的事件系统基于是这个样子。首先设置一个或几个顶层对象,用于管理事件句柄与相关的东西,这里正如我们看到的那样,是用一个叫global的对象。它装载的是元素,因为它是基于事件注册,回调函数都是直接绑定在元素上,后来IE7把内存泄漏的问题放大后,jQuery进一步改进,在unload时把这些注册了事件的元素上面事件全部去掉,现在还没有。在这些元素上有一个叫events的自定义属性,它是一个对象,按事件类弄型管理绑定在它上面的回调函数,目的是让事件按绑定时的顺序执行。当我们触发事件时,并不是直接执行我们绑定的回调函数,因为这里用的是DOM0的事件方式,无论绑定多少个同类型的事件,最后都只一个此类型的。因此都把它们放到一个handle函数中(DE大神的handleEvent)。handle做了三件事,让事件对象总是作为函数的第一个参数,改造事件对象,按顺序执行既定的回调函数。最后我们留意到它有返回值,这是用来决定它是否执行浏览器的默认行为。

我没有闲情把它所有的版本都看了,只看了几个版本,jQuery1.0.4基于还是那个样子。到jQuery1.1增加了几种绑定方式,著名的one,同时对toggle ,hover与ready进行大幅改进。在jQuery中,一个方法基本上都有两个版本,一个jQuery命名空间的静态方法,另一个是jQuery对象的实例方法。实例方法都是对应复数个元素(因为一个jQuery往往包含几个DOM元素),而静态方法基于上是对应一个。实例方法都是往外围调用这些静态方法,因此静态方法的地位相当高。改进的重点都是这些静态方法,因此我重点讲它们。有兴趣可以下jQuery1.1来看看,这时John Resig开始着手解决事件对象的差异问题,为IE的事件对象添加了pageX与pageY与target 属性。unbind与one事件实现得相当傻瓜,因为事件都是用顶层对象管理,把顶层对象的事件删掉,就是unbind了,删除后用一个拷收贝继续执行就是one。hover由于还没有事先搞定relatedTarget ,因此有点复杂。

由于事件系统是个复杂的东西,我还没有开始讲jQuery最新版本的情形,就已达这样的篇幅了。最后我总结下jQuery的事件运行流程吧:用户为元素绑定事件(bind)=>add=>为回调函数设置UUID=>交由顶层对象管理=>handle=>fix=>开始等待用户触发事件或直接调用trigger 。