jQuery-1.9.1源碼分析系列(十)事件系統(tǒng)之事件體系結(jié)構(gòu)
又是一個重磅功能點。
在分析源碼之前分析一下體系結(jié)構(gòu),有助于源碼理解。實際上在jQuery出現(xiàn)之前,Dean Edwards的跨瀏覽器AddEvent()設(shè)計做的已經(jīng)比較優(yōu)秀了;而且jQuery事件系統(tǒng)的設(shè)計思想也是基于該思想的,所以我們先分析一下Dean Edwards前輩的事件綁定。
a. jQuery事件原型——Dean Edwards的跨瀏覽器AddEvent()設(shè)計
源碼解讀
//事件添加方法 function addEvent(element, type, handler) { //保證每個不同的事件響應(yīng)函數(shù)只有唯一一個id if (!handler.$$guid) handler.$$guid = addEvent.guid++; // 給element維護(hù)一個events屬性,初始化為一個空對象。 // element.events的結(jié)構(gòu)類似于 { "click": {...}, "dbclick": {...}, "change": {...} } if (!element.events) element.events = {}; // 試圖取出element.events中當(dāng)前事件類型type對應(yīng)的對象(這個對象更像數(shù)組),賦值給handlers //如果element.events中沒有當(dāng)前事件類型type對應(yīng)的對象則初始化 var handlers = element.events[type]; if (!handlers) { handlers = element.events[type] = {}; // 如果這個element已經(jīng)有了一個對應(yīng)的事件的響應(yīng)方法,例如已經(jīng)有了onclick方法 // 就把element的onclick方法賦值給handlers的0元素,此時handlers的結(jié)構(gòu)就是: // { 0: function(e){...} },這也是為什么addEvent.guid初始化為1的原因,預(yù)留看為0的空間; // 此時element.events的結(jié)構(gòu)就是: { "click": { 0: function(e){...} }, /*省略其他事件類型*/ } if (element["on" + type]) { handlers[0] = element["on" + type]; } } // 把當(dāng)前的事件handler存放到handlers中,handler.$$guid = addEvent.guid++; addEvent.guid = 1; 肯定是從1開始累加的 //因此,這是handlers的結(jié)構(gòu)可能就是 { 0: function(e){...}, 1: function(){}, 2: function(){} 等等... } handlers[handler.$$guid] = handler; //下文定義了一個handleEvent(event)函數(shù),將這個函數(shù),綁定到element的type事件上作為事件入口。 //說明:在element進(jìn)行click時,將會觸發(fā)handleEvent函數(shù),handleEvent函數(shù)將會查找element.events,并調(diào)用相應(yīng)的函數(shù)。可以把handleEvent稱為“主監(jiān)聽函數(shù)” element["on" + type] = handleEvent; }; //計數(shù)器 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是響應(yīng)事件的節(jié)點,這個接點上有events屬性(在addEvent中添加的) //獲取節(jié)點對應(yīng)事件響應(yīng)函數(shù)列表 var handlers = this.events[event.type]; // 循環(huán)響應(yīng)函數(shù)列表執(zhí)行 for (var i in handlers) { //保持正確的作用域,即this關(guān)鍵字 this.$$handleEvent = handlers[i]; this.$$handleEvent(event); } };
重新梳理一下數(shù)據(jù)結(jié)構(gòu),使用一個例子
<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);
經(jīng)過addEvent()函數(shù)之后,當(dāng)前的數(shù)據(jù)結(jié)構(gòu)為:
element: { onclick: handleEvent(event), //click事件的主監(jiān)聽函數(shù) onchage: handleEvent(event), //change事件的主監(jiān)聽函數(shù) events: { click:{//這是一個類數(shù)組 0: f0, //element已有的事件 1: f1, //下標(biāo)1實際上就是f1.$$guid 3: f3 //下標(biāo)3實際上就是f3.$$guid,需要注意的是每一個響應(yīng)事件都有一個唯一的$$guid作為下標(biāo) ... }, change:{//這是一個類數(shù)組 1: f1, 2: f2, 3: f3 } } }
事件系統(tǒng)會根據(jù)調(diào)用addEvent的順序給每個響應(yīng)函數(shù)(也就是addEvent(element, type, handler)中的第三個參數(shù)handler)打上標(biāo)記$$guid。源碼
//保證每個不同的事件響應(yīng)函數(shù)只有唯一一個id if (!handler.$$guid) handler.$$guid = addEvent.guid++;
最終三個響應(yīng)函數(shù)的$$guid標(biāo)記分別是
f1.$$guid = 1
f2.$$guid = 2
f3.$$guid = 3
而根據(jù)源碼中
handlers[handler.$$guid] = handler;
那么某一個函數(shù)在任何事件響應(yīng)函數(shù)集合中的下標(biāo)位置是固定的。比如click和change事件都調(diào)用f3作為響應(yīng)事件,那么f3在element.events.click以及element.events.change中的下標(biāo)位置都是f3.$$guid = 3;即element.events.click[3] = element.events.change[3] = f3。
這個時候假設(shè)又新添了一個事件綁定:addEvent(dom,"focus",f3);那么element.events.focus[3] = f3;這也是對象相比于數(shù)組的方便之處,數(shù)組不可能沒有下標(biāo)0,1,2就直接有3了,但是對象卻可以,此時3是作為對象的一個屬性名稱。
這樣的設(shè)計,其實已經(jīng)具備了jquery事件系統(tǒng)的雛形,包含了幾個最主要的特點:
1)element上的所有事件,將保存到element.events屬性中,不是直接綁定到element上;這樣一個事件可以有無數(shù)個響應(yīng)函數(shù)。
2)handleEvent作為element所有事件的“主監(jiān)聽函數(shù)”,有它統(tǒng)一管理element上的所有函數(shù)。
3)所有瀏覽器都支持element["on" + type]事件綁定方式,跨瀏覽器兼容。
好啦,明白了addEvent的事件結(jié)構(gòu),這個想法確實讓人覺得眼前一亮。下面分析jQuery的事件結(jié)構(gòu)
b. jQuery的事件結(jié)構(gòu)
所有的函數(shù)添加事件都會進(jìn)入jQuery.event.add函數(shù)。該函數(shù)有兩個主要功能:添加事件、附加很多事件相關(guān)信息。我們直接上源碼,源碼思想和Dean Edwards的跨瀏覽器兼容事件添加處理類似。
源碼分析
add: function( elem, types, handler, data, selector ) { var tmp, events, t, handleObjIn, special, eventHandle, handleObj, handlers, type, namespaces, origType, //獲取elem節(jié)點對應(yīng)的緩存數(shù)據(jù) elemData = jQuery._data( elem ); //沒有數(shù)據(jù)或文本/注釋節(jié)點不能附加事件(但是允許附加普通對象) if ( !elemData ) { return; } //調(diào)用者能通過自定義數(shù)據(jù)替換handler if ( handler.handler ) { handleObjIn = handler; handler = handleObjIn.handler; selector = handleObjIn.selector; } //確保handler函數(shù)有唯一的ID,后續(xù)會用來查找/刪除這個handler函數(shù) if ( !handler.guid ) { handler.guid = jQuery.guid++; } //如果是初次進(jìn)入,初始化元素的事件結(jié)構(gòu)和主事件響應(yīng)入口 if ( !(events = elemData.events) ) { events = elemData.events = {}; } if ( !(eventHandle = elemData.handle) ) { eventHandle = elemData.handle = function( e ) { //當(dāng)一個事件被調(diào)用后頁面已經(jīng)卸載,則放棄jQuery.event.trigger()的第二個事件, return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : undefined; }; //將elem作為handle函數(shù)的一個特征防止ie非本地事件引起的內(nèi)存泄露 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; //基于新設(shè)置的類型更新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" ) //用來判斷親密關(guān)系 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添加到元素的處理列表,代理計數(shù)遞增 if ( selector ) { handlers.splice( handlers.delegateCount++, 0, handleObj ); } else { handlers.push( handleObj ); } //跟蹤那個事件曾經(jīng)被使用過,用于事件優(yōu)化 jQuery.event.global[ type ] = true; } //防止ie內(nèi)存泄漏 elem = null; }
依然用實例來說明jQuery的事件結(jié)構(gòu)
<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>
經(jīng)過添加處理環(huán)節(jié),事件添加到了元素上,而且節(jié)點對應(yīng)的緩存數(shù)據(jù)也添加了相應(yīng)的數(shù)據(jù)。結(jié)構(gòu)如下
elemData = jQuery._data( elem ); elemData = { events: { click: {//Array[3] 0: { data: undefined/{...}, guid: 2, //處理函數(shù)的id handler: function dohander(){…}, namespace: "", needsContext: false, origType: "click", selector: "#center",//選擇器,用來區(qū)分不同事件源 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,//委托事件數(shù)量,有selector的才是委托事件 length: 3 } } handle: function ( e ) {…}/*事件處理主入口*/{ elem: document//屬于handle對象的特征 } }
jQuery的處理和Dean Edwards的跨瀏覽器兼容事件添加處理類似,比如為每一個函數(shù)添加guid;使用events對象存放響應(yīng)事件列表,有一個總的事件處理入口handle等。
jQuery做了哪些改進(jìn)?
1)事件數(shù)據(jù)不再直接保存在節(jié)點上,而是使用jQuery緩存系統(tǒng)內(nèi)(內(nèi)部使用的緩存jQuery._data方式存?。?/p>
2)事件委托:綁定到當(dāng)前節(jié)點(例子中當(dāng)前節(jié)點是document根節(jié)點)的處理函數(shù)不僅僅包含當(dāng)前節(jié)點觸發(fā)事件(click)響應(yīng)時處理的事件(例子中selector為undefined時對應(yīng)的處理函數(shù)dot);還代理了其他節(jié)點(例子中的#center節(jié)點)觸發(fā)事件(click)響應(yīng)時處理的事件(例子中selector為"#center"對應(yīng)的處理事件doHandler和dot);委托機(jī)制在后續(xù)分析。
3)增加了很多功能數(shù)據(jù),比如命名空間namespace:這個主要用在自定義事件自定義觸發(fā),比如$(document).on("chua.click",'#center',dot),主動觸發(fā)$("#center").trigger("chua.click")。還有額外數(shù)據(jù)data:雖然沒有看到那個地方有被用到。
到此jQuery的事件結(jié)構(gòu)就清楚了。后面再分析事件的綁定和觸發(fā)以及委托原理。
- AJAX 驗證框架13個
- jquery 框架使用教程 AJAX篇
- Jquery AJAX 框架的使用方法
- 基于JQuery框架的AJAX實例代碼
- javascript之AJAX框架使用說明
- asp.net省市三級聯(lián)動的DropDownList+Ajax的三種框架(aspnet/Jquery/ExtJs)示例
- 簡單的前端js+ajax 購物車框架(入門篇)
- jQuery1.9.1針對checkbox的調(diào)整方法(prop)
- 零基礎(chǔ)學(xué)習(xí)AJAX之AJAX框架
- jQuery 1.9.1源碼分析系列(十)事件系統(tǒng)之綁定事件
- jQuery-1.9.1源碼分析系列(十)事件系統(tǒng)之事件包裝
- Jquery1.9.1源碼分析系列(六)延時對象應(yīng)用之jQuery.ready
- Jquery-1.9.1源碼分析系列(十一)之DOM操作
- jQuery 1.9.1源碼分析系列(十三)之位置大小操作
- jQuery 1.9.1源碼分析系列(十四)之常用jQuery工具
- jQuery1.9.1源碼分析系列(十六)ajax之a(chǎn)jax框架
相關(guān)文章

jQuery EasyUI 開源插件套裝 完全替代ExtJS

jquery hover 不停閃動問題的解決方法(亦為stop()的使用)

使用Browserify配合jQuery進(jìn)行編程的超級指南