jQuery源码学习笔记九
最近几天搞了一个基于事件代理的事件系统,但即便是事件代理还是要依赖于事件注册,因此深入研究了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 。