前文主要介紹了添加事件監聽的方法,本文則主要講刪除事件監聽,以及事件模擬。
jQuery.fn.off
jQuery.fn.off = function( types, selector, fn ) { var handleObj, type; // 如果types是對象,其實現在應該說是type,並且擁有preventDefalut和handleObj if ( types && types.preventDefault && types.handleObj ) { // 通過types獲取handleObj handleObj = types.handleObj; // 轉成字符串來取消事件 jQuery( types.delegateTarget ).off( handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, handleObj.selector, handleObj.handler ); return this; } // 如果types還是對象,那么認為其是是一個map,key對應事件名,value對應處理函數 if ( typeof types === "object" ) { // ( types-object [, selector] ) // 遍歷所有type for ( type in types ) { this.off( type, selector, types[ type ] ); } return this; } // 如果selector為false,或者selector是個函數 if ( selector === false || typeof selector === "function" ) { // ( types [, fn] ) // 等同於傳進來types和fn fn = selector; selector = undefined; } if ( fn === false ) { // 如果fn是false,則定義為一個return false的函數 fn = returnFalse; } // 遍歷所有元素 return this.each(function() { // 使用jQuery.event.remove刪除所有事件處理 jQuery.event.remove( this, types, fn, selector ); }); };
這個方法邏輯還是比較清晰的,嘗試處理各種傳參方式以后,最終都是利用jQuery.event.remove來刪除事件處理函數的。
jQuery.event.remove
jQuery.event.remove = function( elem, types, handler, selector, mappedTypes ) { var j, origCount, tmp, events, t, handleObj, special, handlers, type, namespaces, origType, // 通過內部緩存獲取對象相關數據 elemData = jQuery.hasData( elem ) && jQuery._data( elem ); // 如果沒有相關緩存數據,或者緩存中沒有相關處理列表,則這個對象沒事件可刪除 if ( !elemData || !(events = elemData.events) ) { // 退出 return; } // types可能是通過空格分隔的多個type,轉成數組 types = ( types || "" ).match( core_rnotwhite ) || [""]; t = types.length; // 遍歷所有type while ( t-- ) { // 分解type和namespace tmp = rtypenamespace.exec( types[t] ) || []; // 得到type type = origType = tmp[1]; // 得到namespace namespaces = ( tmp[2] || "" ).split( "." ).sort(); // 如果type是undefined,即原來的type是.xxx.xxx之類的命名空間 if ( !type ) { // 從事件列表讀取所有事件 for ( type in events ) { // 添加命名空間,刪除掉這些事件 jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); } continue; } // 嘗試得到特殊事件 special = jQuery.event.special[ type ] || {}; type = ( selector ? special.delegateType : special.bindType ) || type; handlers = events[ type ] || []; tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); // 刪除掉滿足的事件 origCount = j = handlers.length; while ( j-- ) { // 得到事件對象 handleObj = handlers[ j ]; // 參數mappedTypes存在或當前事件和handleObj中的當前事件相同 if ( ( mappedTypes || origType === handleObj.origType ) && // 並且參數handler不存在,或handler的ID與handleObj的ID相同 ( !handler || handler.guid === handleObj.guid ) && // 並且沒有命名空間,或者是handleObj的命名空間子集 ( !tmp || tmp.test( handleObj.namespace ) ) && // 並且沒有selector,或者selector與handleObj的selector相同, // 或者selector為"**"(表示任意)並且handleObj的selector存在 ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { // 全部滿足則刪除掉當前事件對象 handlers.splice( j, 1 ); // 如果handleObj有selector if ( handleObj.selector ) { handlers.delegateCount--; } // 如果特殊事件remove存在,則調用special.remove // 應該和special.add對應,目前應當沒什么用 if ( special.remove ) { special.remove.call( elem, handleObj ); } } } // 如果緩存中本來存在事件處理對象,且當前沒有事件處理對象 // 證明全部在上面循環中刪除掉了,就清除掉 // 避免潛在的特殊事件處理程序無限遞歸 if ( origCount && !handlers.length ) { // 則嘗試用special.teardown刪除事件對handle的綁定 if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { // 不成功則使用removeEventListener刪除綁定 // 這里雖然還是這么寫但實際上就是removeEventListener了 jQuery.removeEvent( elem, type, elemData.handle ); } // 刪除緩存中對應事件處理函數列表 delete events[ type ]; } } // 如果緩存events已經空了,該對象沒有任何事件綁定了 if ( jQuery.isEmptyObject( events ) ) { // 在緩存中刪除handle delete elemData.handle; // 清除掉events jQuery._removeData( elem, "events" ); } };
- 實際上,主要是刪除時要判斷事件、處理函數、命名空間等是否匹配,匹配才能刪除。
- 還有就是,如果該事件的處理函數列隊空了就需要對該事件解綁定。
- 如果改時間的事件列表都空了,那么就將主處理器,事件列表都刪掉。
然后剩下的解綁定函數都是由jQuery.fn.off擴展來的。
jQuery.fn.unbind
jQuery.fn.unbind: function( types, fn ) { return this.off( types, null, fn ); };
jQuery.fn.undelegate
jQuery.fn.undelegate = function( selector, types, fn ) { // ( namespace ) or ( selector, types [, fn] ) return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); };
jQuery.fn.trigger
jQuery.fn.trigger = function( type, data ) { return this.each(function() { jQuery.event.trigger( type, data, this ); }); };
jQuery.fn.trigger方法直接調用jQuery.event.trigger來模擬發消息。
下面的jQuery.fn.triggerHandler也是通過jQuery.event.trigger來模擬發消息。
jQuery.fn.triggerHandler = function( type, data ) { var elem = this[0]; if ( elem ) { return jQuery.event.trigger( type, data, elem, true ); } };
jQuery.event.trigger
jQuery.event.trigger = function( event, data, elem, onlyHandlers ) { var i, cur, tmp, bubbleType, ontype, handle, special, // 需要觸發事件的所有元素隊列 eventPath = [ elem || document ], // 指定事件類型 type = event.type || event, // 事件是否有命名空間,有則分割成數組 namespaces = event.namespace ? event.namespace.split(".") : []; cur = tmp = elem = elem || document; // 對於text和comment節點不進行事件處理 if ( elem.nodeType === 3 || elem.nodeType === 8 ) { return; } // 僅對focus/blur事件變種成focusin/out進行處理 // 如果瀏覽器原生支持focusin/out,則確保當前不觸發他們 if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { return; } // 如果type有命名空間 if ( type.indexOf(".") >= 0 ) { // 重新組裝事件 namespaces = type.split("."); type = namespaces.shift(); namespaces.sort(); } // 看看是否需要改成ontype形式 ontype = type.indexOf(":") < 0 && "on" + type; // 看看這個是不是由jQuery.Event生成的實例,否則用jQuery.Event改造 event = event[ jQuery.expando ] ? event : new jQuery.Event( type, typeof event === "object" && event ); // 對event預處理 event.isTrigger = true; event.namespace = namespaces.join("."); event.namespace_re = event.namespace ? new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : null; // 清除數據,以重新使用 event.result = undefined; // 如果事件沒有觸發元素,則用elem代替 if ( !event.target ) { event.target = elem; } // 如果data為空,則傳入處理函數的是event,否則由data和event組成 data = data == null ? [ event ] : jQuery.makeArray( data, [ event ] ); // 嘗試通過特殊事件進行處理,必要時候退出函數 special = jQuery.event.special[ type ] || {}; if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { return; } // 如果需要冒泡,特殊事件不需要阻止冒泡,且elem不是window對象 if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { // 冒泡時是否需要轉成別的事件(用於事件模擬) bubbleType = special.delegateType || type; // 如果不是變形來的foucusin/out事件 if ( !rfocusMorph.test( bubbleType + type ) ) { // 則定義當前元素師父節點 cur = cur.parentNode; } // 遍歷自身及所有父節點 for ( ; cur; cur = cur.parentNode ) { // 推入需要觸發事件的所有元素隊列 eventPath.push( cur ); // 存一下循環中最后一個cur tmp = cur; } // 如果循環中最后一個cur是document,那么事件是需要最后觸發到window對象上的 // 將window對象推入元素隊列 if ( tmp === (elem.ownerDocument || document) ) { eventPath.push( tmp.defaultView || tmp.parentWindow || window ); } } // 觸發所有該事件對應元素的事件處理器 i = 0; // 遍歷所有元素,並確保事件不需要阻止冒泡 while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { // 先確定事件綁定類型是delegateType還是bindType event.type = i > 1 ? bubbleType : special.bindType || type; // 確保緩存中該元素對應事件中包含事件處理器, // 則取出主處理器(jQuery handle)來控制所有分事件處理器 handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); // 如果主處理器(jQuery handle)存在 if ( handle ) { // 觸發處理器 handle.apply( cur, data ); } // 取出原生事件處理器elem.ontype // 比如click事件就是elem.onclick handle = ontype && cur[ ontype ]; // 如果原生事件處理器存在,看看需不需要阻止事件在瀏覽器上的默認動作 if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { event.preventDefault(); } } // 保存事件類型,因為這時候事件可能變了 event.type = type; // 如果不需要阻止默認動作,立即執行 if ( !onlyHandlers && !event.isDefaultPrevented() ) { // 嘗試通過特殊事件觸發默認動作 if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { // 調用一個原生的DOM方法具有相同名稱的名稱作為事件的目標。 // 例如對於事件click,elem.click()是觸發該事件 // 並確保不對window對象阻止默認事件 if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) { // 防止我們觸發FOO()來觸發其默認動作時,onFOO事件又觸發了 tmp = elem[ ontype ]; // 清除掉該事件監聽 if ( tmp ) { elem[ ontype ] = null; } // 當我們已經將事件向上起泡時,防止相同事件再次觸發 jQuery.event.triggered = type; // 觸發事件 elem[ type ](); // 完成清除標記 jQuery.event.triggered = undefined; // 事件觸發完了,可以把監聽重新綁定回去 if ( tmp ) { elem[ ontype ] = tmp; } } } } return event.result; };
模擬觸發為了讓事件模型在各瀏覽器上表現一致,花了不少的心思。
反過來說,瀏覽器事件模型表現不一致,真心折磨人……orz