1. 前言:
在看Aaron的jquery原始碼解讀時候,看到事件系統那塊,作者提到了Dean Edwards的新增事件的設計,於是就點進去看了看。首先讓我吃驚的是,程式碼非常少,寥寥幾十行,非常簡單。於是我就仔細的看了看(如果程式碼太多,可能就直接不看了)。
這段程式碼是Dean Edwards在2005年寫的了,那時候還沒有jquery。但是它的設計思路確實和jquery的事件系統有些相似,即便是在9年之後的今天。
於是把這段程式碼仔細研究,並在此跟大家分享以下。它將幫助你更好的理解jquery的事件系統。
先把原始碼粘上,:
1 function addEvent(element, type, handler) { 2 // assign each event handler a unique ID 3 if (!handler.$$guid) handler.$$guid = addEvent.guid++; 4 // create a hash table of event types for the element 5 if (!element.events) element.events = {}; 6 // create a hash table of event handlers for each element/event pair 7 var handlers = element.events[type]; 8 if (!handlers) { 9 handlers = element.events[type] = {}; 10 // store the existing event handler (if there is one) 11 if (element["on" + type]) { 12 handlers[0] = element["on" + type]; 13 } 14 } 15 // store the event handler in the hash table 16 handlers[handler.$$guid] = handler; 17 // assign a global event handler to do all the work 18 element["on" + type] = handleEvent; 19 }; 20 // a counter used to create unique IDs 21 addEvent.guid = 1; 22 23 function removeEvent(element, type, handler) { 24 // delete the event handler from the hash table 25 if (element.events && element.events[type]) { 26 delete element.events[type][handler.$$guid]; 27 } 28 }; 29 30 function handleEvent(event) { 31 // grab the event object (IE uses a global event object) 32 event = event || window.event; 33 // get a reference to the hash table of event handlers 34 var handlers = this.events[event.type]; 35 // execute each event handler 36 for (var i in handlers) { 37 this.$$handleEvent = handlers[i]; 38 this.$$handleEvent(event); 39 } 40 };
2. 該設計的優點:
Dean Edwards在文章中提到了該設計的幾個優點:
it performs no object detection 不執行物件檢測,不理解何意。。。
it does not use the addeventListener
/attachEvent
methods 不使用addeventListener
/attachEvent
方法,因為這兩個方法分別由不同的瀏覽器支援,使用時候需要判斷。 但是,在Dean Edwards提供的下載程式碼中,應用到了addeventListener。
it keeps the correct scope (the this
keyword) 保持正確的作用域,即this關鍵字
it passes the event object correctly 正確的傳遞event物件
it is entirely cross-browser (it will probably work on IE4 and NS4) 保證瀏覽器相容性,甚至支援IE4和NetSape4(2005年)
and from what I can tell it does not leak memory 不會出現記憶體洩漏
3. 程式碼解讀:
3.1 事件新增方法addEvent():
1 //事件新增方法 2 function addEvent(element, type, handler) { 3 4 // assign each event handler a unique ID 5 // 為傳入的每個事件初始化一個唯一的id 6 if (!handler.$$guid) handler.$$guid = addEvent.guid++; //下文:addEvent.guid = 1; 7 8 // create a hash table of event types for the element 9 // 給element維護一個events屬性,初始化為一個空物件。 10 // element.events的結構類似於 { "click": {...}, "dbclick": {...}, "change": {...} } 11 // 即element.events是一個物件,其中每個事件型別又會對應一個物件 12 if (!element.events) element.events = {}; 13 14 // create a hash table of event handlers for each element/event pair 15 // 試圖取出element.events中當前事件型別type對應的物件,賦值給handlers 16 var handlers = element.events[type]; 17 if (!handlers) { 18 handlers = element.events[type] = {}; 19 //如果handlers是undefined,則初始化為空物件 20 21 22 // store the existing event handler (if there is one) 23 // 如果這個element已經有了一個方法,例如已經有了onclick方法 24 // 就把element的onclick方法賦值給handlers的0元素,此時handlers的結構就是: 25 // { 0: function(e){...} } 26 // 此時element.events的結構就是: { "click": { 0: function(e){...} }, /*省略其他事件型別*/ } 27 if (element["on" + type]) { 28 handlers[0] = element["on" + type]; 29 } 30 } 31 // store the event handler in the hash table 32 // 把當前的事件handler存放到handlers中,handler.$$guid = addEvent.guid++; addEvent.guid = 1; 肯定是從1開始累加的 33 // 因此,這是handlers的結構就是 { 0: function(e){...}, 1: function(){}, 2: function(){} 等等... } 34 handlers[handler.$$guid] = handler; 35 36 // assign a global event handler to do all the work 37 // 下文定義了一個handleEvent(event)函式 38 // 將這個函式,繫結到element的type事件上。 說明:在element進行click時,將會觸發handleEvent函式,handleEvent函式將會查詢element.events,並呼叫相應的函式。可以把handleEvent稱為“主監聽函式” 39 element["on" + type] = handleEvent; 40 }; 41 42 // a counter used to create unique IDs 43 addEvent.guid = 1;
以上程式碼都給出了詳細的註釋,應該能看明白了。重新梳理以下資料結構,經過addEvent()函式之後,當前的資料結構為:(假如type = 'click')
element: { onclick: handleEvent(event), /*下文定義的函式*/ events: { click:{ 0: function(){...}, /*element已有的click事件*/ 1: function(){...}, 2: function(){...} /*.......其他事件......*/ }, change:{ /*省略*/ }, dbclick:{ /*省略*/ } } }
這樣的設計,其實已經具備了jquery事件系統的雛形,包含了兩個最主要的特點:
- element上的所有事件,將儲存到element.events屬性中,不是直接繫結到element上;
- handleEvent作為element所有事件的“主監聽函式”,有它統一管理element上的所有函式。
接著往下看:
3.2 主監聽函式handleEvent(event):
1 //主監聽函式 2 function handleEvent(event) { 3 // grab the event object (IE uses a global event object) 4 // 在IE中,event需要通過window.event獲取 5 event = event || window.event; 6 7 // get a reference to the hash table of event handlers 8 // 根據事件型別在events中獲取事件集合(events的資料結構,參考addEvent方法的註釋) 9 var handlers = this.events[event.type]; 10 // 注意!注意! 這裡的this不是window,而是element物件,因為上文 element["on" + type] = handleEvent; 11 // 所以在程式執行時,handleEvent已經作為了element的一個屬性,它的作用域是element,即this === element 12 13 // execute each event handler 14 // 迴圈執行handlers集合裡的所有函式 另外,這裡執行事件時傳遞的event,無論在什麼瀏覽器下,都是正確的 15 for (var i in handlers) { 16 this.$$handleEvent = handlers[i]; 17 this.$$handleEvent(event); 18 19 //此處為何要把handlers[i]賦值給this.$$handleEvent,然後在執行呢? 20 //而不是直接執行handlers[i](event)? 21 //跟記憶體洩漏有關? 22 //我也沒看明白,大家自己思考的,知道的可以分享給大家。 23 } 24 };
以上就是主監聽函式的實現過程,都做了註釋,也不叫好理解,有個問題,已經在程式碼中有黃色背景標出來了,有了解的,也麻煩分享給大家。
jquery的主監聽函式執行時候實現的比較複雜,但是思路上和這個是一樣的。
3.3 移除函式事件:
1 //移除函式事件 2 function removeEvent(element, type, handler) { 3 // delete the event handler from the hash table 4 // 迴圈element.events[type],根據handler的唯一的id,進行delete 5 if (element.events && element.events[type]) { 6 delete element.events[type][handler.$$guid]; 7 } 8 };
移除函式比較簡單,不多解釋。
4. 總結
這段程式碼相對於jquery的事件系統,少了事件的代理,以及模擬的時間冒泡。不考慮代理,當然就簡單許多。
但是它已經點出了jquery事件系統的原型,理解它,能幫助你更好的理解jquery事件系統。
補充:司徒正美的新書《javascript框架設計》中專門有一節講解:11.4 Dean Edward的addEvent.js原始碼分析 260頁