這篇文章主要介紹了jQuery-1.9.1源碼分析系列(十)事件系統之事件體系結構的相關資料,需要的朋友可以參考下
又是一個重磅功能點。
在分析源碼之前分析一下體系結構,有助于源碼理解。實際上在jQuery出現之前,Dean Edwards的跨瀏覽器AddEvent()設計做的已經比較優秀了;而且jQuery事件系統的設計思想也是基于該思想的,所以我們先分析一下Dean Edwards前輩的事件綁定。
a. jQuery事件原型——Dean Edwards的跨瀏覽器AddEvent()設計
源碼解讀
- //事件添加方法
- function addEvent(element, type, handler) {
- //保證每個不同的事件響應函數只有唯一一個id
- if (!handler.$guid) handler.$guid = addEvent.guid++;
- // 給element維護一個events屬性,初始化為一個空對象。
- // element.events的結構類似于 { "click": {...}, "dbclick": {...}, "change": {...} }
- if (!element.events) element.events = {};
- // 試圖取出element.events中當前事件類型type對應的對象(這個對象更像數組),賦值給handlers
- //如果element.events中沒有當前事件類型type對應的對象則初始化
- var handlers = element.events[type];
- if (!handlers) {
- handlers = element.events[type] = {};
- // 如果這個element已經有了一個對應的事件的響應方法,例如已經有了onclick方法
- // 就把element的onclick方法賦值給handlers的0元素,此時handlers的結構就是:
- // { 0: function(e){...} },這也是為什么addEvent.guid初始化為1的原因,預留看為0的空間;
- // 此時element.events的結構就是: { "click": { 0: function(e){...} }, /*省略其他事件類型*/ }
- if (element["on" + type]) {
- handlers[0] = element["on" + type];
- }
- }
- // 把當前的事件handler存放到handlers中,handler.$guid = addEvent.guid++; addEvent.guid = 1; 肯定是從1開始累加的
- //因此,這是handlers的結構可能就是 { 0: function(e){...}, 1: function(){}, 2: function(){} 等等... }
- handlers[handler.$guid] = handler;
- //下文定義了一個handleEvent(event)函數,將這個函數,綁定到element的type事件上作為事件入口。
- //說明:在element進行click時,將會觸發handleEvent函數,handleEvent函數將會查找element.events,并調用相應的函數。可以把handleEvent稱為“主監聽函數”
- element["on" + type] = handleEvent;
- };
- //計數器
- 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) {
- //兼容ie
- event = event || window.event;
- //this是響應事件的節點,這個接點上有events屬性(在addEvent中添加的)
- //獲取節點對應事件響應函數列表
- var handlers = this.events[event.type];
- // 循環響應函數列表執行
- for (var i in handlers) {
- //保持正確的作用域,即this關鍵字
- this.$handleEvent = handlers[i];
- this.$handleEvent(event);
- }
- };
重新梳理一下數據結構,使用一個例子
- <input type="text" id="chua" onClick="f0();">
- function f0(){...}
- function f1(){...}
- function f2(){...}
- function f3(){...}
- var dom = document.getElementById("chua");
- addEvent(dom,"click",f1);
- addEvent(dom,"change",f1);
- addEvent(dom,"change",f2);
- addEvent(dom,"click",f3);
- addEvent(dom,"change",f3);
經過addEvent()函數之后,當前的數據結構為:
- element: {
- onclick: handleEvent(event), //click事件的主監聽函數
- onchage: handleEvent(event), //change事件的主監聽函數
- events: {
- click:{//這是一個類數組
- 0: f0, //element已有的事件
- 1: f1, //下標1實際上就是f1.$guid
- 3: f3 //下標3實際上就是f3.$guid,需要注意的是每一個響應事件都有一個唯一的$guid作為下標
- ...
- },
- change:{//這是一個類數組
- 1: f1,
- 2: f2,
- 3: f3
- }
- }
- }
事件系統會根據調用addEvent的順序給每個響應函數(也就是addEvent(element, type, handler)中的第三個參數handler)打上標記$$guid。源碼
- //保證每個不同的事件響應函數只有唯一一個id
- f (!handler.$guid) handler.$guid = addEvent.guid++;
最終三個響應函數的$$guid標記分別是
f1.$$guid = 1
f2.$$guid = 2
f3.$$guid = 3
而根據源碼中
- handlers[handler.$guid] = handler;
那么某一個函數在任何事件響應函數集合中的下標位置是固定的。比如click和change事件都調用f3作為響應事件,那么f3在element.events.click以及element.events.change中的下標位置都是f3.$$guid = 3;即element.events.click[3] = element.events.change[3] = f3。
這個時候假設又新添了一個事件綁定:addEvent(dom,"focus",f3);那么element.events.focus[3] = f3;這也是對象相比于數組的方便之處,數組不可能沒有下標0,1,2就直接有3了,但是對象卻可以,此時3是作為對象的一個屬性名稱。
這樣的設計,其實已經具備了jquery事件系統的雛形,包含了幾個最主要的特點:
1)element上的所有事件,將保存到element.events屬性中,不是直接綁定到element上;這樣一個事件可以有無數個響應函數。
2)handleEvent作為element所有事件的“主監聽函數”,有它統一管理element上的所有函數。
3)所有瀏覽器都支持element["on" + type]事件綁定方式,跨瀏覽器兼容。
好啦,明白了addEvent的事件結構,這個想法確實讓人覺得眼前一亮。下面分析jQuery的事件結構
b. jQuery的事件結構
所有的函數添加事件都會進入jQuery.event.add函數。該函數有兩個主要功能:添加事件、附加很多事件相關信息。我們直接上源碼,源碼思想和Dean Edwards的跨瀏覽器兼容事件添加處理類似。
源碼分析
- add: function( elem, types, handler, data, selector ) {
- var tmp, events, t, handleObjIn,
- special, eventHandle, handleObj,
- handlers, type, namespaces, origType,
- //獲取elem節點對應的緩存數據
- elemData = jQuery._data( elem );
- //沒有數據或文本/注釋節點不能附加事件(但是允許附加普通對象)
- if ( !elemData ) {
- return;
- }
- //調用者能通過自定義數據替換handler
- if ( handler.handler ) {
- handleObjIn = handler;
- handler = handleObjIn.handler;
- selector = handleObjIn.selector;
- }
- //確保handler函數有唯一的ID,后續會用來查找/刪除這個handler函數
- if ( !handler.guid ) {
- handler.guid = jQuery.guid++;
- }
- //如果是初次進入,初始化元素的事件結構和主事件響應入口
- if ( !(events = elemData.events) ) {
- events = elemData.events = {};
- }
- if ( !(eventHandle = elemData.handle) ) {
- eventHandle = elemData.handle = function( e ) {
- //當一個事件被調用后頁面已經卸載,則放棄jQuery.event.trigger()的第二個事件,
- return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ?
- jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
- undefined;
- };
- //將elem作為handle函數的一個特征防止ie非本地事件引起的內存泄露
- eventHandle.elem = elem;
- }
- //多個事件使用空格隔開的處理
- //如jQuery(...).bind("mouseover mouseout", fn);
- //core_rnotwhite = //S+/g;匹配空白字符
- types = ( types || "" ).match( core_rnotwhite ) || [""];
- t = types.length;
- while ( t-- ) {
- //rtypenamespace = /^([^.]*)(?:/.(.+)|)$/;
- //獲取命名空間和原型事件
- tmp = rtypenamespace.exec( types[t] ) || [];
- type = origType = tmp[1];
- namespaces = ( tmp[2] || "" ).split( "." ).sort();
- //如果事件改變其類型,使用special事件處理器來處理更改后的事件類型
- special = jQuery.event.special[ type ] || {};
- //如果選擇器已定義,確定special事件API類型,否則給他一個類型
- type = ( selector ? special.delegateType : special.bindType ) || type;
- //基于新設置的類型更新special
- special = jQuery.event.special[ type ] || {};
- // handleObj貫穿整個事件處理
- handleObj = jQuery.extend({
- type: type,
- origType: origType,
- data: data,
- handler: handler,
- guid: handler.guid,
- selector: selector,
- // For use in libraries implementing .is(). We use this for POS matching in `select`
- //"needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?://(" +
- //whitespace + "*((?:-//d)?//d*)" + whitespace + "*//)|)(?=[^-]|$)", "i" )
- //用來判斷親密關系
- needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
- namespace: namespaces.join(".")
- }, handleObjIn );
- //初次使用時初始化事件處理器隊列
- if ( !(handlers = events[ type ]) ) {
- handlers = events[ type ] = [];
- handlers.delegateCount = 0;
- //非自定義事件,如果special事件處理器返回false,則只能使用addEventListener/attachEvent
- if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
- //給元素綁定全局事件
- if ( elem.addEventListener ) {
- elem.addEventListener( type, eventHandle, false );
- } else if ( elem.attachEvent ) {
- elem.attachEvent( "on" + type, eventHandle );
- }
- }
- }
- //自定義事件綁定
- if ( special.add ) {
- special.add.call( elem, handleObj );
- if ( !handleObj.handler.guid ) {
- handleObj.handler.guid = handler.guid;
- }
- }
- //將事件對象handleObj添加到元素的處理列表,代理計數遞增
- if ( selector ) {
- handlers.splice( handlers.delegateCount++, 0, handleObj );
- } else {
- handlers.push( handleObj );
- }
- //跟蹤那個事件曾經被使用過,用于事件優化
- jQuery.event.global[ type ] = true;
- }
- //防止ie內存泄漏
- elem = null;
- }
依然用實例來說明jQuery的事件結構
- <div id="#center"></div>
- <script>
- function dohander(){console.log("dohander")};
- function dot(){console.log("dot");}
- $(document).on("click",'#center',dohander)
- .on("click",'#center',dot)
- .on("click",dot);
- </script>
經過添加處理環節,事件添加到了元素上,而且節點對應的緩存數據也添加了相應的數據。結構如下
- elemData = jQuery._data( elem );
- elemData = {
- events: {
- click: {//Array[3]
- 0: {
- data: undefined/{...},
- guid: 2, //處理函數的id
- handler: function dohander(){…},
- namespace: "",
- needsContext: false,
- origType: "click",
- selector: "#center",//選擇器,用來區分不同事件源
- type: "click"
- }
- 1: {
- data: undefined/{...},
- guid: 3,
- handler: function dot(){…},
- namespace: "",
- needsContext: false,
- origType: "click",
- selector: "#center",
- type: "click"
- }
- 2: {
- data: undefined,
- guid: 3,
- handler: function dot(){…},
- namespace: "",
- needsContext: false,
- origType: "click",
- selector: undefined,
- type: "click"
- }
- delegateCount: 2,//委托事件數量,有selector的才是委托事件
- length: 3
- }
- }
- handle: function ( e ) {…}/*事件處理主入口*/{
- elem: document//屬于handle對象的特征
- }
- }
jQuery的處理和Dean Edwards的跨瀏覽器兼容事件添加處理類似,比如為每一個函數添加guid;使用events對象存放響應事件列表,有一個總的事件處理入口handle等。
jQuery做了哪些改進?
1)事件數據不再直接保存在節點上,而是使用jQuery緩存系統內(內部使用的緩存jQuery._data方式存取)
2)事件委托:綁定到當前節點(例子中當前節點是document根節點)的處理函數不僅僅包含當前節點觸發事件(click)響應時處理的事件(例子中selector為undefined時對應的處理函數dot);還代理了其他節點(例子中的#center節點)觸發事件(click)響應時處理的事件(例子中selector為"#center"對應的處理事件doHandler和dot);委托機制在后續分析。
3)增加了很多功能數據,比如命名空間namespace:這個主要用在自定義事件自定義觸發,比如$(document).on("chua.click",'#center',dot),主動觸發$("#center").trigger("chua.click")。還有額外數據data:雖然沒有看到那個地方有被用到。
到此jQuery的事件結構就清楚了。后面再分析事件的綁定和觸發以及委托原理。
新聞熱點
疑難解答
圖片精選