最近幾天搞了一個基於事件代理的事件系統,但即便是事件代理還是要依賴於事件註冊,因此深入研究了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 IDsaddEvent.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 。