jQuery event(下)


 前文主要介紹了添加事件監聽的方法,本文則主要講刪除事件監聽,以及事件模擬。

 

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM