代码改变世界

jQuery event(下)

2013-01-23 08:36  Justany_WhiteSnow  阅读(4120)  评论(2编辑  收藏  举报

 前文主要介绍了添加事件监听的方法,本文则主要讲删除事件监听,以及事件模拟。

 

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