綜觀一個系統的發展,無非是發現一個問題就把它獨立出來解決掉,因此它的所有模組(或者分支)其實針對獨立的問題,這樣我們對這些問題若有什麼更好的解決方案,替換相應的模組便是,要不代碼混雜在一起沒法看了。上一部分就提到了,attachEvent的事件列隊出了些問題,我們要手動構建列隊了。所謂列隊就是有先有後的問題,因此DE大神搞了一個全域的uuid,分配給每個回呼函數。但要注意,每個回調所針對的事件來源與事件類型,因此這不可能一個數組搞定。看:
el.attachEvent("onmouseenter",function(){alert(window.event.type)}) | | | | | | | | | |事件來源 監聽器 事件類型 回呼函數 事件對象
事件來源,其實el也一定是事件來源,如某同類型事件從文檔樹下冒泡上來,或者頂層對象要捕獲它下面的某個同類型事件,那它就成了currentTarget。為簡單起見,我稱它為事件來源。這個事件來源可以綁定很多監聽器,每個監聽器可以針對不同的事件類型,當然這也意味著有許多回呼函數。當我們發現attachEvent多個同類型事件時,回呼函數的次序出錯時,意味著,如果我們還要它時,每種事件類型只能綁定監聽器一次。那不就意味著,一個監聽器只有一個回呼函數,因為只有一個次序肯定不會出錯!但怎麼添加更多回呼函數呢?嗯,我們需要一個數組。數組裡面的回呼函數才是我們自己添加的,然後一個for迴圈執行它們就是!
var queue = []; el.attachEvent("onmouseenter",function(){ var e = window.event; for(var i=0,n=queue.length;i<n;i++){ queue[i].call(this,e)//queue[i]為我們自己的回呼函數,this指向el } }); queue.push(callback)
這樣就實現監聽事件與添加回呼函數相分離。但是一個事件類型就要搞一個queue,而且這些事件類型即使是同一類型還有事件來源之分。那會產出巨量的全域變數。因此我們必須找個地方放置它們。DE大神在要監聽的每個元素節點(或者文檔對象什麼的)上設定一個events屬性,那是一個對象,然後以type-object的形式儲存,type即為事件類型,object為一個對象,鍵為他的那個uuid,值為回呼函數。下面是他的addEvent函數的一部分,完整代碼這裡(要翻牆):
function addEvent(element, type, handler) { if (!handler.$$guid) handler.$$guid = addEvent.guid++; if (!element.events) element.events = {}; var handlers = element.events[type]; if (!handlers) { handlers = element.events[type] = {}; if (element["on" + type]) { handlers[0] = element["on" + type]; } } handlers[handler.$$guid] = handler; element["on" + type] = handleEvent; };
下面是它的events的結構:
element.events = { "click":{ 1:fn1, 3:fn3, 4:fn4 }, "mousemove":{ 2:fn2, 5:fn5 } }
這樣儲存結構註定了只能用最慢的for...in迴圈來遍曆。嘛,限於當時人們的眼界,大神已經做得很好了。最佳的結構應該能對應我上面給出的樣本,應該是這個樣子:
element.events = { "click":[fn1,fn3,fn4], "mousemove":[fn2,fn5] }
縱觀DE的事件大神的事件系統,可是當時是複雜的,是具模組化的,每個針對不同的問題進行處理:
addEvent //為事件來源某一個事件類型添加一個監聽器(它的回呼函數總是主處理器),並把回呼函數放置到事件來源的events屬性上removeEvent //為事件來源某一個事件類型逐一刪減回呼函數,handleEvent//主處理器,用於在裡面遍曆我們自己添加的回呼函數fixEvent//也是在主處理器中執行,並只針對IE的事件來源對象,為它添加標準瀏覽器的兩個方法
IE7發布後,引入新的記憶體流失。在IE7中,DOM對象不會被CG程式回收,只有離開頁面時會被回收,但如果這時還被東西引用著就完蛋了,所以我們要清空元素節點上面的東西。如果我們把這個毛病解決了就很完美了,無奈DE大神不幹了,這個問題留給其他繼續發展的架構搞定。其中之一就是jQuery,它的事件系統就是基本DE大神的。jQuery面臨的任務有如下幾個:
- 支援更多的事件類型,如FF下的滾輪事件,DOMMouseScroll是不能通過onXXX添加到事件來源上的。
- 分離事件來源那個events屬性,交由新式的緩衝系統集中管理。
- 讓IE與標準瀏覽器的事件來源對象更加標準化。
至於事件代理,那是更後的事了。它最初都是由外掛程式引入的,然後逐步發展到今日的規模。至於上面三點,我在《javascript 跨瀏覽器的事件系統》系列給出的相應的思路了:用緩衝系統把element.events從元素上分離出來,取而代之是一個輕量的uuid,其對應的緩衝體儲存事件類型與回呼函數的映射;建立一個偽事件對象將原生事件對象包裹起來,在它的原型添加w3c的事件方法;在fix函數中修正其事件屬性,如左中右鍵,座標,事件來源等亂七八糟的東西,這樣就幾乎以假亂真了;在handle函數,我們監視每次回呼函數執行的結果,如果為undefined就讓它冒泡,如果為false就禁止冒泡與預設行為,如果isImmediatePropagationStopped的執行結果為true就斷開迴圈,換言之,禁止同類型的回呼函數的繼續執行。這個東西是從Flash學回來的,稱之為stopImmediatePropagation。當然,對於DOMNodeInserted、 DOMNodeRemoved、 DOMNodeRemovedFromDocument、 DOMNodeInsertedIntoDocument、 DOMAttrModifiedonclick這樣進階的事件,onXXX是無能為力了,因此必須請回attachEvent與addEventListener。DOMMouseScroll。
jQuery.event = { add:function(){},//為事件來源某一個事件類型添加一個監聽器(它的回呼函數總是主處理器),並把回呼函數放到緩衝體中 remove:function(){},//把回呼函數從緩衝體中刪除,如果某一類型為空白,則移除相應的監聽器(DE大神的不會) fix:function(){},//將事件對象用偽事件對象封裝起來,因為事件對象的屬性都是唯讀,這樣才能添加與修正更多標準的屬性 handle:function(){},//根據事件類型從緩衝體中取出,遍曆執行,並根據回呼函數的結果執行阻止冒泡與預設行為等方法 trigger:function(){}//實現跨平台的充許傳參的事件指派 } jQuery.Event = function(){}//為事件對象添加w3c的那幾個標準方法與少量屬性
如果類庫看得多的話,就會知道全世界都在設法類比mouseenter/leave,這是IE特有的事件,它們的優越性可見這篇文章《Goodbye mouseover, hello mouseenter》。由於標準瀏覽器不支援,我們必須用其他類似事件類比它們。不用說,最近它們的是mouseover,mouseout。對於這樣特殊的事件,還有domReady,jQuery引入了special系統,一個事件的子系統,繞了大圈讓原生事件回到類比事件的事件列隊中。當然這樣做有一個弊端,mouseente/leave在標準瀏覽器的事件類型總是錯誤的……看來,這special系統還不成熟呢!
jQuery還引進入命名空間與事件指派,這對以後事件代理非常有用。那麼第三部分繼續!
//利用同一個命名空間綁定三個事件 //$('a').bind('keydown.key keypress.key keyup.key', function () { alert("nasamidesu"); }); //然後卸載時就輕鬆了 //$('a').unbind('.key');