1. 程式人生 > >jQuery原始碼-事件繫結函式bind/delegate/click...

jQuery原始碼-事件繫結函式bind/delegate/click...

 前言:我用的jQuery版本為v2.0.1,jQuery繫結事件的介面有bind/delegate/click等其他方法,但是他們最終呼叫的還是this.on作為入口

但是jQuery提供兩種繫結機制

1.普通事件繫結

2.委託繫結事件

這兩中的區別有很大的不同,只是在作用上。委託繫結事件機制能給瀏覽器帶來很大的效能優化。不過兩種可以防止瀏覽器的記憶體洩露( 特別是在IE中)

今天我用click方法走下普通事件繫結的原始碼,大同小異

一://又是一種簡寫模式 迴圈生成事件函式
jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
"change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) {

// Handle event binding
jQuery.fn[ name ] = function( data, fn ) {

return arguments.length > 0 ?
this.on( name, null, data, fn ) :// click null callback undefined
this.trigger( name );
};
});

1.jQuery用簡寫模式,使基本事件函式迴圈到jQuery..fn中,可以讓外部呼叫 

二:// jquery on
on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
//click null  callback undefined undefined
//click 'a' callback undefined undefined
var type, origFn;

// Types can be a map of types/handlers
if ( typeof types === "object" ) {
// ( types-Object, selector, data )
if ( typeof selector !== "string" ) {
// ( types-Object, data )
data = data || selector;
selector = undefined;
}
for ( type in types ) {
this.on( type, selector, data, types[ type ], one );
}
return this;
}
//引數調整
if ( data == null && fn == null ) {
// ( types, fn )
fn = selector;
data = selector = undefined;
} else if ( fn == null ) {
if ( typeof selector === "string" ) {
// ( types, selector, fn )
fn = data; //callback 
data = undefined;//undefined
} else {
// ( types, data, fn )
fn = data; //callback
data = selector;//null
selector = undefined;// undefined

}
}
if ( fn === false ) {
fn = returnFalse;
} else if ( !fn ) {
return this;
}

if ( one === 1 ) {
origFn = fn;
fn = function( event ) {
// Can use an empty set, since event contains the info
jQuery().off( event );
return origFn.apply( this, arguments );
};
// Use same guid so caller can remove using origFn
fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
}
return this.each( function() {
//  elem , click,callback,null,undefined
// elem , click,callback,null,'a'
jQuery.event.add( this, types, fn, data, selector );
});
},

1.this.on()大部分工作主要是修復引數.

2.主要繫結工作在 jQuery.event.add( this, types, fn, data, selector );中

三://  elem,click,callback,null,undefined
//  elem,click,callback,null,'a'
add: function( elem, types, handler, data, selector ) {

var tmp, events, t, handleObjIn,
special, eventHandle, handleObj,
handlers, type, namespaces, origType,
elemData = jQuery._data( elem );//{} 空物件== jQuery.cache[id]

// Don't attach events to noData or text/comment nodes (but allow plain objects)
if ( !elemData ) {
return;
}

// Caller can pass in an object of custom data in lieu of the handler
if ( handler.handler ) {
handleObjIn = handler;
handler = handleObjIn.handler;
selector = handleObjIn.selector;
}

// Make sure that the handler has a unique ID, used to find/remove it later
if ( !handler.guid ) {
handler.guid = jQuery.guid++;//2
}

// Init the element's event structure and main handler, if this is the first

if ( !(events = elemData.events) ) {
events = elemData.events = {};//初始化為空物件
}

if ( !(eventHandle = elemData.handle) ) {
eventHandle = elemData.handle = function( e ) {
//下面這段程式碼是點選真正事件的時候,執行程式碼
// Discard the second event of a jQuery.event.trigger() and
// when an event is called after a page has unloaded
return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ?
jQuery.event.dispatch.apply( eventHandle.elem, arguments ) ://elem event
undefined;
};

// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
eventHandle.elem = elem;
}

// Handle multiple events separated by a space
types = ( types || "" ).match( core_rnotwhite ) || [""];//click

t = types.length;

while ( t-- ) {
///^([^.]*)(?:\.(.+)|)$/
tmp = rtypenamespace.exec( types[t] ) || [];

type = origType = tmp[1];//click
namespaces = ( tmp[2] || "" ).split( "." ).sort();//[""]

// There *must* be a type, no attaching namespace-only handlers
if ( !type ) {
continue;
}


// If event changes its type, use the special event handlers for the changed type
special = jQuery.event.special[ type ] || {};

// If selector defined, determine special event api type, otherwise given type
type = ( selector ? special.delegateType : special.bindType ) || type;//clcik

// Update special based on newly reset type
special = jQuery.event.special[ type ] || {};


// handleObj is passed to all event handlers
handleObj = jQuery.extend({
type: type,
origType: origType,
data: data,
handler: handler,
guid: handler.guid,
selector: selector,
needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
namespace: namespaces.join(".")
}, handleObjIn );


// Init the event handler queue if we're the first
if ( !(handlers = events[ type ]) ) {
handlers = events[ type ] = [];
handlers.delegateCount = 0;

//任何事件都是用addEventListener/attachEvent繫結的
// Only use addEventListener/attachEvent if the special events handler returns false

if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
// Bind the global event handler to the element
if ( elem.addEventListener ) {//非IE支援
//eventHandle 其實就是一個執行分派回撥函式的一個入口

elem.addEventListener( type, eventHandle, false );


} else if ( elem.attachEvent ) {//IE支援
elem.attachEvent( "on" + type, eventHandle );
}
}
}

if ( special.add ) {
special.add.call( elem, handleObj );


if ( !handleObj.handler.guid ) {
handleObj.handler.guid = handler.guid;
}
}

// Add to the element's handler list, delegates in front
if ( selector ) {
//委託事件處理 handlers 存放這一些回撥函式
//handlers 引用 events[type] , events 引用 elemData.events , elemData 又是elem的事件快取

//0,0,obj
//如果之前綁定了事件,則會在陣列頭部插入handleObj,不管用不用delegete方法,委託回撥函式總是在普通回撥函式後面執行
handlers.splice( handlers.delegateCount++, 0, handleObj );



} else {
//普通事件處理   將組裝的回撥函式push進handlers  = events[type] = elemData.events[type]
handlers.push( handleObj );

}


// Keep track of which events have ever been used, for event optimization
jQuery.event.global[ type ] = true;
}


// Nullify elem to prevent memory leaks in IE
//將回調函式壓進回撥函式列表後,在置空elem,釋放記憶體.防止記憶體洩露
elem = null;
},

1.elemData = jQuery._data( elem );//{}空物件 == jQuery.cache[id]   

獲取該元素在資料快取倉庫中的快取資料,如果沒有的話,就構造為空的物件

2.handler.guid = jQuery.guid++; // Make sure that the handler has a unique ID, used to find/remove it later

給傳進來的回撥函式唯一一個ID,以備以後定位和刪除

3.events = elemData.events = {};//初始化為空物件  如果elemData為空物件,則構造events屬性 並且也為空物件

4.eventHandle = elemData.handle = function( e ) {
//下面這段程式碼是點選真正事件的時候,執行程式碼
// Discard the second event of a jQuery.event.trigger() and
// when an event is called after a page has unloaded
return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ?
jQuery.event.dispatch.apply( eventHandle.elem, arguments ) ://elem event
undefined;
};

// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
eventHandle.elem = elem;

同理如果elemData為空物件,則構造handle屬性 並且為一個函式,其中jQuery.event.dispatch.apply( eventHandle.elem, arguments ) 就是分派,也就是事件發生時真正執行的程式碼,稍後詳細介紹內部

5.// Init the event handler queue if we're the first
if ( !(handlers = events[ type ]) ) {
handlers = events[ type ] = [];
handlers.delegateCount = 0;

//任何事件都是用addEventListener/attachEvent繫結的
// Only use addEventListener/attachEvent if the special events handler returns false

if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
// Bind the global event handler to the element
if ( elem.addEventListener ) {//非IE支援
//eventHandle 其實就是一個執行分派回撥函式的一個入口

elem.addEventListener( type, eventHandle, false );


} else if ( elem.attachEvent ) {//IE支援
elem.attachEvent( "on" + type, eventHandle );
}
}
}

初始化基本的屬性,和在DOM繫結全域性函式,其中handlers.delegateCount = 0 表示普通事件繫結,否則為委託事件繫結

6.if ( selector ) {
//委託事件處理 handlers 存放這一些回撥函式
//handlers 引用 events[type] , events 引用 elemData.events , elemData 又是elem的事件快取

//0,0,obj
//如果之前綁定了事件,則會在陣列頭部插入handleObj,不管用不用delegete方法,委託回撥函式總是在普通回撥函式後面執行
handlers.splice( handlers.delegateCount++, 0, handleObj );



} else {
//普通事件處理   將組裝的回撥函式push進handlers  = events[type] = elemData.events[type]
handlers.push( handleObj );

}

2個分支都是將將組裝的回撥函式push進handlers  = events[type] = elemData.events[type] push進handlers中,只是過程不一樣,

委託繫結,是陣列頭部插入,而普通事件是陣列尾部插入,到這裡基本的繫結初始化工作已經完成,事件資料已經初始化到資料倉庫中了,並且為DOM綁定了全域性回撥函式,

接下來我們詳細解析下全域性回撥函式

7.eventHandle = elemData.handle = function( e ) {
//下面這段程式碼是點選真正事件的時候,執行程式碼
// Discard the second event of a jQuery.event.trigger() and
// when an event is called after a page has unloaded
return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ?
jQuery.event.dispatch.apply( eventHandle.elem, arguments ) ://elem event
undefined;
};

8.dispatch: function( event ) {

// Make a writable jQuery.Event from the native event object
//讓事件物件 在IE和其他瀏覽器相容,並且附加一些自己的屬性

event = jQuery.event.fix( event );//在fix函式中上下文this = jQuery.event

var i, ret, handleObj, matched, j,
handlerQueue = [],
args = core_slice.call( arguments ),
handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [],//通過獲取elem的事件快取,得到之前所儲存的回撥函式
special = jQuery.event.special[ event.type ] || {};

// Use the fix-ed jQuery.Event rather than the (read-only) native event
args[0] = event;

event.delegateTarget = this;

// Call the preDispatch hook for the mapped type, and let it bail if desired
//分派之前的鉤子

if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {

return;
}

// Determine handlers
handlerQueue = jQuery.event.handlers.call( this, event, handlers );

// Run delegates first; they may want to stop propagation beneath us
i = 0;
//迴圈遍歷執行handlerQueue的回撥函式
//如果matched存在,而且 停止冒泡狀態機的標識為return false 
while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {

event.currentTarget = matched.elem;//事件的目標物件

j = 0;
//如果handleObj存在,而且 停止冒泡狀態機的標識為return false 
while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {

// Triggered event must either 1) have no namespace, or
// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).

if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {

event.handleObj = handleObj;

event.data = handleObj.data;

//在這裡執行回撥函式 將上下文替換成最初繫結的元素
//特別注意這裡的args,其實就是我們回撥中的event實參,args將作為handleObj.handler(event){}中的event引數
ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
.apply( matched.elem, args );

if ( ret !== undefined ) {
if ( (event.result = ret) === false ) {
event.preventDefault();
event.stopPropagation();
}
}
}
}
}


// Call the postDispatch hook for the mapped type
//分派之後的鉤子

if ( special.postDispatch ) {
special.postDispatch.call( this, event );
}

return event.result;//undefined
},

event = jQuery.event.fix( event );//在fix函式中上下文this = jQuery.event   

// Make a writable jQuery.Event from the native event object
//讓事件物件 在IE和其他瀏覽器相容,並且附加一些自己的屬性

9。fix: function( event ) {

//如果該事件物件已經修復過,直接返回
if ( event[ jQuery.expando ] ) {
return event;
}


// Create a writable copy of the event object and normalize some properties
var i, prop, copy,
type = event.type,//click
originalEvent = event,
fixHook = this.fixHooks[ type ];//undefined

//keyHooks.filter 和 mouseHooks.filter 兩個方法分別用於修改鍵盤和滑鼠事件的屬性相容性問題,用於統一介面。
//時間處理的鉤子機制
if ( !fixHook ) {
this.fixHooks[ type ] = fixHook =
rmouseEvent.test( type ) ? this.mouseHooks ://滑鼠事件特有屬性
rkeyEvent.test( type ) ? this.keyHooks ://按鍵事件特有屬性
{};
}

//fixHook.props 滑鼠/鍵盤事件特有的屬性,將基礎的屬性和特有的屬性合併,為了相容
//如果是滑鼠事件,則將滑鼠的屬性相容,同理按鍵事件也是一樣
copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;

//構造新的自定義的event物件(原有的物件為新物件的一個屬性)
event = new jQuery.Event( originalEvent );

i = copy.length;//25
//copy是修復屬性的集合,如果原始物件和新構造的物件屬性名相同,覆蓋,如果不相同,這新增到新的物件中,相容不同瀏覽器(IE)
while ( i-- ) {
prop = copy[ i ];
event[ prop ] = originalEvent[ prop ];
}

// Support: IE<9IE9之前的事件的目標物件是用srcElement表示,而其他瀏覽器使用target表示
//好像現在的IE瀏覽器基本和其他瀏覽器一致
// Fix target property (#1925)
if ( !event.target ) {
event.target = originalEvent.srcElement || document;
}

// Support: Chrome 23+, Safari?
// Target should not be a text node (#504, #13143)
if ( event.target.nodeType === 3 ) {
//如果點選的是文字節點,則尋找他的父節點
event.target = event.target.parentNode;
}

// Support: IE<9
// For mouse/key events, metaKey==false if it's undefined (#3368, #11328)
event.metaKey = !!event.metaKey;

return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;// event
},

(1)//keyHooks.filter 和 mouseHooks.filter 兩個方法分別用於修改鍵盤和滑鼠事件的屬性相容性問題,用於統一介面。
//時間處理的鉤子機制
if ( !fixHook ) {
this.fixHooks[ type ] = fixHook =
rmouseEvent.test( type ) ? this.mouseHooks ://滑鼠事件特有屬性
rkeyEvent.test( type ) ? this.keyHooks ://按鍵事件特有屬性
{};
}

(2)//fixHook.props 滑鼠/鍵盤事件特有的屬性,將基礎的屬性和特有的屬性合併,為了相容
//如果是滑鼠事件,則將滑鼠的屬性相容,同理按鍵事件也是一樣
copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;

(3)//構造新的自定義的event物件(原有的物件為新物件的一個屬性)
event = new jQuery.Event( originalEvent );

(4)//copy是修復屬性的集合,如果原始物件和新構造的物件屬性名相同,覆蓋,如果不相同,這新增到新的物件中,相容不同瀏覽器(IE)
while ( i-- ) {
prop = copy[ i ];
event[ prop ] = originalEvent[ prop ];
}

// Support: IE<9IE9之前的事件的目標物件是用srcElement表示,而其他瀏覽器使用target表示
//好像現在的IE瀏覽器基本和其他瀏覽器一致
// Fix target property (#1925)
if ( !event.target ) {
event.target = originalEvent.srcElement || document;
}

// Support: Chrome 23+, Safari?
// Target should not be a text node (#504, #13143)
if ( event.target.nodeType === 3 ) {
//如果點選的是文字節點,則尋找他的父節點
event.target = event.target.parentNode;
}

// Support: IE<9
// For mouse/key events, metaKey==false if it's undefined (#3368, #11328)
event.metaKey = !!event.metaKey;

(5)return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;// event  過濾 處理一些IE與其他瀏覽器的某些屬性名

10.handlerQueue = jQuery.event.handlers.call( this, event, handlers );     將handlers處理,然後push進handlerQueue中

11.

//在這裡執行回撥函式 將上下文替換成最初繫結的元素
//特別注意這裡的args,其實就是我們回撥中的event實參,args將作為handleObj.handler(event){}中的event引數
ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
.apply( matched.elem, args );
(1)最後迴圈執行傳進來的回撥函式

12.貼出中間過程走過的處理函式

fix()函式中

(1)//構造新的自定義的event物件(原有的物件為新物件的一個屬性)
event = new jQuery.Event( originalEvent );

jQuery.Event = function( src, props ) {

// Allow instantiation without the 'new' keyword
if ( !(this instanceof jQuery.Event) ) {
return new jQuery.Event( src, props );
}


// Event object
if ( src && src.type ) {
this.originalEvent = src;
this.type = src.type;

// Events bubbling up the document may have been marked as prevented
// by a handler lower down the tree; reflect the correct value.
//狀態機 表示是否預設阻止
this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false ||
src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse;


// Event type
} else {
this.type = src;
}

// Put explicitly provided properties onto the event object
//如果有自定義提供的屬性,也合併到this物件中
if ( props ) {
jQuery.extend( this, props );
}

// Create a timestamp if incoming event doesn't have one
this.timeStamp = src && src.timeStamp || jQuery.now();

//jQuery.expando 作為event是否被修復過的標識
// Mark it as fixed
this[ jQuery.expando ] = true;
};


// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
jQuery.Event.prototype = {
//狀態機制
isDefaultPrevented: returnFalse,
isPropagationStopped: returnFalse,
isImmediatePropagationStopped: returnFalse,


preventDefault: function() {
var e = this.originalEvent;


this.isDefaultPrevented = returnTrue;
if ( !e ) {
return;
}


// If preventDefault exists, run it on the original event
if ( e.preventDefault ) {
e.preventDefault();


// Support: IE
// Otherwise set the returnValue property of the original event to false
} else {
e.returnValue = false;
}
},
stopPropagation: function() {
var e = this.originalEvent;


this.isPropagationStopped = returnTrue;
if ( !e ) {
return;
}
// If stopPropagation exists, run it on the original event
if ( e.stopPropagation ) {
e.stopPropagation();
}


// Support: IE
// Set the cancelBubble property of the original event to true
e.cancelBubble = true;
},
stopImmediatePropagation: function() {
this.isImmediatePropagationStopped = returnTrue;
this.stopPropagation();
}
};

(2)return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;// event

根據事件型別呼叫對用事件型別的過濾函式,鉤子機制,用來相容一些屬性名

13.handlerQueue = jQuery.event.handlers.call( this, event, handlers );

//修復的event物件,回撥函式物件
handlers: function( event, handlers ) {

var sel, handleObj, matches, i,
handlerQueue = [],
delegateCount = handlers.delegateCount,//0表示普通事件
cur = event.target;//<div id="aaron">Aron test</div>a#a

// Find delegate handlers
// Black-hole SVG <use> instance trees (#13180)
// Avoid non-left-click bubbling in Firefox (#3861)
//如果為委託事件,則執行下列程式碼
if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) {


/* jshint eqeqeq: false */
//cur != this 迴圈到cur等於當前委託繫結的元素
for ( ; cur != this; cur = cur.parentNode || this ) {
/* jshint eqeqeq: true */

// Don't check non-elements (#13208)
// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)

if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) {
matches = [];
for ( i = 0; i < delegateCount; i++ ) {

handleObj = handlers[ i ];


// Don't conflict with Object.prototype properties (#13203)
sel = handleObj.selector + " ";//a a a 

if ( matches[ sel ] === undefined ) {

matches[ sel ] = handleObj.needsContext ?// 1 0 0
jQuery( sel, this ).index( cur ) >= 0 :
jQuery.find( sel, this, null, [ cur ] ).length;//呼叫Sizzle庫,由complile動態生成查詢函式,並且執行返回查詢結果

}

//如果查詢到a,則把handleObj物件(包含回撥函式及一些屬性)push matches中
if ( matches[ sel ] ) {
matches.push( handleObj );
}
}
//如果matches有回撥物件壓入,則將matches整個壓入handlerQueue回撥佇列中

if ( matches.length ) {
handlerQueue.push({ elem: cur, handlers: matches });
}



}
}
}

// Add the remaining (directly-bound) handlers
//0<1

if ( delegateCount < handlers.length ) {
handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });
}

return handlerQueue;
},

該函式特別有針對委託事件型別的處理,其中

jQuery.find( sel, this, null, [ cur ] ).length; //呼叫Sizzle庫,由complile動態生成查詢函式,並且執行返回查詢結果

14.結束分析過程

四:總結

1.它將回調函式不是直接繫結在DOM上,而是放在資料倉庫中,DOM元素中存放資料倉庫鑰匙jQuery[expand]

2.幾乎將IE高低版本與其他瀏覽器相容了,小到DOM元素的每個屬性

3.每次繫結完成後,都會讓傳進來的elem= null ,大大降低了瀏覽器記憶體洩露的可能性

4.將IE中的event和其他瀏覽器的event統一起來運用。

5.簡簡單單的繫結一個點選事件,jQuery卻做了如此多的工作,每一步都非常有意思

6.上面程式碼中很多 handler = events[type] = elemData.events程式碼,其實在JS中 針對於物件和陣列,他們為引用關係,改變其中一個其他的都會改變

所以這也是為什麼dispatch只傳進了elem,event引數,因為可以通過elem獲取快取資料

7.只有看懂程式碼,進而才能分析其框架的架構和思路,寫的好累。

8.最後上傳一張借鑑的圖.資料倉庫模型


五:接下來會分析下委託程式碼,跟普通又什麼區別