代码改变世界

jQuery event(上)

2013-01-22 12:05  Justany_WhiteSnow  阅读(9571)  评论(4编辑  收藏  举报

由于jQuery事件管理内容比较多,所以进行了分段,这篇文章主要讲的是事件的绑定。

 

jQuery.fn.on

在选择元素上绑定一个或多个事件的事件处理函数。

jQuery.fn.on = function( types, selector, data, fn, /*INTERNAL*/ one ) {
    var origFn, type;

    // types可以是一个由types/handlers组成的map对象
    if ( typeof types === "object" ) {
        // 如果selector不是字符串
        // 则将传参由( types-Object, selector, data )变成( types-Object, data )
        if ( typeof selector !== "string" ) {
            data = data || selector;
            selector = undefined;
        }
        //遍历所有type
        for ( type in types ) {
            //添加type事件处理函数
            this.on( type, selector, data, types[ type ], one );
        }
        return this;
    }

    // 如果data为空,且fn为空
    if ( data == null && fn == null ) {
        // 则传参由( types, selector )变成( types, fn )
        fn = selector;
        data = selector = undefined;
    // 否则如果只是fn为空
    } else if ( fn == null ) {
        // 如果selector为字符串
        if ( typeof selector === "string" ) {
            // 则传参从( types, selector, data )变成( types, selector, fn )
            fn = data;
            data = undefined;
        } else {
            // 否则传参从( type, selector, data )变成( types, data, fn )
            fn = data;
            data = selector;
            selector = undefined;
        }
    }
    
    //……弄了半天其实就是在模拟重载而已……囧rz
    
    if ( fn === false ) {
        //如果fn为false则变成一个return false的函数
        fn = returnFalse;
    } else if ( !fn ) {
        //如果fn现在还不存在,则直接return this
        return this;
    }

    // 如果one为1
    if ( one === 1 ) {
        // 保存fn
        origFn = fn;
        // 重新定义fn
        fn = function( event ) {
            // 这个事件只用一次,用完就用off取消掉。
            jQuery().off( event );
            return origFn.apply( this, arguments );
        }
        // 使用相同的ID,为了未来好删除事件
        fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
    }
    // 对所有用jQuery.event.add来添加
    return this.each( function() {
        jQuery.event.add( this, types, fn, data, selector );
    });
};

文档中对selector的描述是:

  一个选择器字符串用于过滤器的触发事件的选择器元素的后代。如果选择的< null或省略,当它到达选定的元素,事件总是触发。

  A selector string to filter the descendants of the selected elements that trigger the event. If the selector isnull or omitted, the event is always triggered when it reaches the selected element.

说得貌似很悬乎,不过其实主要是在delegate绑定事件中去过滤元素的一些用不着的后代的。

 

jQuery.event.add

jQuery.event.add = function( elem, types, handler, data, selector ) {

    var handleObjIn, eventHandle, tmp,
        events, t, handleObj,
        special, handlers, type, namespaces, origType,
        // 通过内部缓存获取元素数据
        elemData = jQuery._data( elem );

    // 不会没有数据或者text、comment节点添加事件
    if ( !elemData ) {
        return;
    }

    // 如果handler是个包含handler和selector的对象
    if ( handler.handler ) {
        // 则定位必要的参数
        handleObjIn = handler;
        handler = handleObjIn.handler;
        selector = handleObjIn.selector;
    }

    // 如果handler没有ID,则给个ID给他
    // 用于未来寻找或者删除handler
    if ( !handler.guid ) {
        handler.guid = jQuery.guid++;
    }

    // 如果缓存数据中没有events数据
    if ( !(events = elemData.events) ) {
        // 则初始化events
        events = elemData.events = {};
    }
    // 如果缓存数据中没有handle数据
    if ( !(eventHandle = elemData.handle) ) {
        // 定义事件处理函数
        eventHandle = elemData.handle = function( e ) {
            // 取消jQuery.event.trigger第二次触发事件
            // 以及装卸后的事件
            return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ?
                jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
                undefined;
        };
        // 定义事件处理器对应的元素,用于防止IE非原生事件中的内存泄露
        eventHandle.elem = elem;
    }

    // 事件可能是通过空格键分隔的字符串,所以将其变成字符串数组
    types = ( types || "" ).match( core_rnotwhite ) || [""];
    // 事件的长度
    t = types.length;
    // 遍历所有事件
    while ( t-- ) {
        // 尝试取出事件的namespace,如aaa.bbb.ccc
        tmp = rtypenamespace.exec( types[t] ) || [];
        // 取出事件,如aaa
        type = origType = tmp[1];
        // 取出事件命名空间,如bbb.ccc,并根据"."分隔成数组
        namespaces = ( tmp[2] || "" ).split( "." ).sort();

        // 事件是否会改变当前状态,如果会则使用特殊事件
        special = jQuery.event.special[ type ] || {};

        // 根据是否已定义selector,决定使用哪个特殊事件api,如果没有非特殊事件,则用type
        type = ( selector ? special.delegateType : special.bindType ) || type;

        // 更具状态改变后的特殊事件
        special = jQuery.event.special[ type ] || {};

        // 组装用于特殊事件处理的对象
        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 );

        // 初始化事件处理列队,如果是第一次使用
        if ( !(handlers = events[ type ]) ) {
            handlers = events[ type ] = [];
            handlers.delegateCount = 0;

            // 如果获取特殊事件监听方法失败,则使用addEventListener进行添加事件,和attachEvent说88了
            if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
                if ( elem.addEventListener ) {
                    elem.addEventListener( type, eventHandle, false );
                }
            }
        }

        // 通过特殊事件add处理事件
        if ( special.add ) {
            // 添加事件
            special.add.call( elem, handleObj );

            // 设置处理函数的ID
            if ( !handleObj.handler.guid ) {
                handleObj.handler.guid = handler.guid;
            }
        }

        // 将事件处理函数推入处理列表
        if ( selector ) {
            handlers.splice( handlers.delegateCount++, 0, handleObj );
        } else {
            handlers.push( handleObj );
        }

        // 表示事件曾经使用过,用于事件优化
        jQuery.event.global[ type ] = true;
    }

    // 设置为null避免IE中循环引用导致的内存泄露
    elem = null;
};

从这里的源代码看,

  • 对于没有特殊事件特有监听方法和普通事件都用addEventListener来添加事件了。
  • 而又特有监听方法的特殊事件,则用了另一种方式来添加事件。

special.add从2.0的源代码来看,似乎没用用到,看起来是遗留问题,未来也可以根据这个扩展事件模型。

 

jQuery.event.dispatch

我们先走第一个分支,也就是通过addEventListener触发jQuery.event.dispatch。

jQuery.event.dispatch = function( event ) {

    // 重写原生事件对象,变成一个可读写的对象,方便未来修改、扩展
    event = jQuery.event.fix( event );

    var i, j, ret, matched, handleObj,
        handlerQueue = [],
        // 把参数转成数组
        args = core_slice.call( arguments ),
        // 从内部数据中查找该元素的对应事件处理器列表中的对应处理器,否则为空数组
        handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [],
        // 尝试将事件转成特殊事件
        special = jQuery.event.special[ event.type ] || {};

    // 将参数数组第一个元素换成重写的事件对象
    args[0] = event;
    event.delegateTarget = this;
    
    // 尝试使用特殊事件的preDispatch钩子来绑定事件,并在必要时退出
    if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
        return;
    }

    // 组装事件处理包{elem, handlerObjs}(这里是各种不同元素)的队列
    handlerQueue = jQuery.event.handlers.call( this, event, handlers );

    // Run delegates first; they may want to stop propagation beneath us
    i = 0;
    // 遍历事件处理包{elem, handlerObjs}(取出来则对应一个包了),且事件不需要阻止冒泡
    while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
        // 定义当前Target为事件处理对象对应的元素
        event.currentTarget = matched.elem;

        j = 0;
        // 如果事件处理对象{handleObjs}存在(一个元素可能有很多handleObjs),且事件不需要立刻阻止冒泡
        while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {
            // 触发的事件必须满足其一:
            // 1) 没有命名空间
            // 2) 有命名空间,且被绑定的事件是命名空间的一个子集
            if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {
            
                event.handleObj = handleObj;
                event.data = handleObj.data;

                // 尝试通过特殊事件获取处理函数,否则使用handleObj中保存的handler(所以handleObj中还保存有handler)
                ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
                        .apply( matched.elem, args );

                // 如果处理函数存在
                if ( ret !== undefined ) {
                    // 如果处理函数返回值是false,则阻止冒泡,阻止默认动作
                    if ( (event.result = ret) === false ) {
                        event.preventDefault();
                        event.stopPropagation();
                    }
                }
            }
        }
    }

    // 尝试通过special.postDispatch勾住这个映射关系,未来可以优化
    if ( special.postDispatch ) {
        special.postDispatch.call( this, event );
    }

    // 返回事件函数
    return event.result;
};

这里有许多handle相关的东西,具体关系参见以下后面的函数。

 

jQuery.event.handlers

jQuery.event.handlers = function( event, handlers ) {
    var i, matches, sel, handleObj,
        handlerQueue = [],
        delegateCount = handlers.delegateCount,
        // 当前事件触发元素
        cur = event.target;

    // Find delegate handlers
    // Black-hole SVG <use> instance trees (#13180)
    // Avoid non-left-click bubbling in Firefox (#3861)
    // 如果有delegateCount,代表该事件是delegate类型的绑定
    // 找出所有delegate的处理函数列队
    if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) {
        
        // 遍历元素及元素父级节点
        for ( ; cur != this; cur = cur.parentNode || this ) {

            // 防止单机被禁用的元素时触发事件
            if ( 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 + " ";
                    
                    // 如果matches上没有绑定该选择器数量
                    if ( matches[ sel ] === undefined ) {
                        // 在matches上绑定该选择器数量
                        matches[ sel ] = handleObj.needsContext ?
                            // 得出选择器数量,并赋值
                            jQuery( sel, this ).index( cur ) >= 0 :
                            jQuery.find( sel, this, null, [ cur ] ).length;
                    }
                    // 再次确定是否绑定选择器数量
                    if ( matches[ sel ] ) {
                        // 是则将事件处理对象推入
                        matches.push( handleObj );
                    }
                }
                // 如果得到的matches里有事件处理对象
                if ( matches.length ) {
                    // 组装成事件处理包(暂时这么叫吧),推入事件处理包队列
                    handlerQueue.push({ elem: cur, handlers: matches });
                }
            }
        }
    }

    // 如果还有事件剩余,则将剩余的装包,推入列队
    if ( delegateCount < handlers.length ) {
        handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });
    }

    return handlerQueue;
}

从这里我们可以看出delegate绑定的事件和普通绑定的事件是如何分开的。

对应一个元素,一个event.type的事件处理对象队列在缓存里只有一个。

区分delegate绑定和普通绑定的方法是:delegate绑定从队列头部推入,而普通绑定从尾部推入,通过记录delegateCount来划分,delegate绑定和普通绑定。

 

special.setup

// 支持: Firefox 10+
// 创建冒泡的focus和blur事件,即focusin和focusout
if ( !jQuery.support.focusinBubbles ) {
    jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {

        // 通过这个参数来记录某人focusin/focusout
        var attaches = 0,
            // 
            handler = function( event ) {
                jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );
            };

        // 对需要修复的特殊事件添加方法
        jQuery.event.special[ fix ] = {
            setup: function() {
                if ( attaches++ === 0 ) {
                    document.addEventListener( orig, handler, true );
                }
            },
            teardown: function() {
                if ( --attaches === 0 ) {
                    document.removeEventListener( orig, handler, true );
                }
            }
        };
    });
}

第二个分支special.setup方法主要是来在Firefox中模拟focusin和focusout事件的,因为各大主流浏览器只有他不支持这两个事件。

由于这两个方法支持事件冒泡,所以可以用来进行事件代理。

 

jQuery.event.simulate

然后再利用jQuery.event.simulate来模拟事件触发。

jQuery.event.simulate = function( type, elem, event, bubble ) {
    // 重写事件
    var e = jQuery.extend(
        new jQuery.Event(),
        event,
        { type: type,
            isSimulated: true,
            originalEvent: {}
        }
    );
    // 如果要冒泡
    if ( bubble ) {
        // 利用jQuery.event.trigger模拟触发事件
        jQuery.event.trigger( e, null, elem );
    } else {
        // 否则利用jQuery.event.dispatch来执行处理
        jQuery.event.dispatch.call( elem, e );
    }
    // 如果需要阻止默认操作,则阻止
    if ( e.isDefaultPrevented() ) {
        event.preventDefault();
    }
}

 

jQuery.fn.one

jQuery.fn.one = function( types, selector, data, fn ) {
    return this.on( types, selector, data, fn, 1 );
};

运行一次就直接调用this.on,然后在最后参数设置成只用1次就行了。

 

jQuery.fn.bind

jQuery.fn.bind = function( types, data, fn ) {
    return this.on( types, null, data, fn );
};

这个也是通过this.on扩展的。

 

jQuery.fn.live

jQuery.fn.live = function( types, data, fn ) {
    jQuery( this.context ).on( types, this.selector, data, fn );
    return this;
};

 

jQuery.fn.delegate

jQuery.fn.delegate = function( selector, types, data, fn ) {
    return this.on( types, selector, data, fn );
};

可见jQuery.fn.on是事件添加的核心方法,几乎所有事件添加方法都是由这一方法扩展出来的。