jQuery源码分析--event事件绑定(上)

Posted 小章鱼哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jQuery源码分析--event事件绑定(上)相关的知识,希望对你有一定的参考价值。

上文提到,jquery的事件绑定有bind(),delegate()和one()以及live()方式。我用的jQuery2.1.3版本,live()已经被废弃了。

bind(),delegate()和one()的内部源码。

//7491行
bind: function( types, data, fn ) {
        return this.on( types, null, data, fn );
    },
//7498行
delegate: function( selector, types, data, fn ) {
        return this.on( types, selector, data, fn );
    },
//7474行 
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 ) :
            this.trigger( name );
    };
});
//4859行
one: function( types, selector, data, fn ) {
        return this.on( types, selector, data, fn, 1 );
    },

以下这几个函数全都指向了on方法。

jQuery.fn.on

//4806行
on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
        var origFn, type;

        // Types can be a map of types/handlers
        // types可以是一个由types/handlers组成的map对象
        if ( typeof types === "object" ) {
            // ( types-Object, selector, data )
            // 如果selector不是字符串
        // 则将传参由( types-Object, selector, data )变成( types-Object, data )
            if ( typeof selector !== "string" ) {
                // ( types-Object, data )
                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, fn )
            fn = selector;
            data = selector = undefined;
        // 否则如果只是fn为空
        } else if ( fn == null ) {
            // 如果selector为字符串
            if ( typeof selector === "string" ) {
                // 传参变成( types, selector, fn )
                fn = data;
                data = undefined;
            } else {
                // 否则传参变成( types, data, fn )
                fn = data;
                data = selector;
                selector = undefined;
            }
        }
        if ( fn === false ) {
           //如果fn为false则变成一个return false的函数
            fn = returnFalse;
        } else if ( !fn ) {
           //如果fn现在还不存在,则直接return this
            return this;
        }
        // 如果one为1,one参数为1说明是函数只执行一次即被废除
        if ( one === 1 ) {
            //把fn赋值给另一个变量保存
            origFn = fn;
            //重新定义fn
            fn = function( event ) {
                // Can use an empty set, since event contains the info
                 // 这个事件只用一次,用完就用off取消掉。
                jQuery().off( event );
                return origFn.apply( this, arguments );
            };
            // Use same guid so caller can remove using origFn
             // 使用相同的ID,为了未来好删除事件
            fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
        }
        //最后,对于所有情况,调用jQuery.event.add函数继续处理
        return this.each( function() {
            jQuery.event.add( this, types, fn, data, selector );
        });
    },

可见,jquery.fn.on函数有以下功能

  • 对每个不同的输入情况进行重载
  • 重载之后交给jQuery.event.add函数处理

总的来说就是:通过 on 绑定事件,分析传递的数据,加工变成 add 能够识别的数据。

jQuery.event.add

//4083行
add: function( elem, types, handler, data, selector ) {

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

        // 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
        //in lieu of 代替
        //定位handler的两个参数handler和selector
        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
        //如果handler没有id,则给它定义一个id,确保每个handler都有一个id,用于未来查找和删除
        if ( !handler.guid ) {
            handler.guid = jQuery.guid++;
        }

        // Init the element‘s event structure and main handler, if this is the first
        //如果是第一次创建,初始化元素的事件结构和主要handler
        // 如果缓存数据中没有events数据
        if ( !(events = elemData.events) ) {
            // 则初始化events
            events = elemData.events = {};
        }
         // 如果缓存数据中没有handle数据
        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
                // 丢弃jQuery.event.trigger第二次触发事件以及当一个页面被卸载后调用事件
                return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ?
                    jQuery.event.dispatch.apply( elem, arguments ) : undefined;
            };
        }

        // Handle multiple events separated by a space
        // 事件可能是通过空格键分隔的字符串,所以将其变成字符串数组
        types = ( types || "" ).match( rnotwhite ) || [ "" ];
          // 事件的长度
        t = types.length;
          // 遍历所有事件
        while ( t-- ) {
            //尝试取出事件的命名空间,就如上文提到的click.plugin,plugin是命名空间
            tmp = rtypenamespace.exec( types[t] ) || [];
            //取出事件
            type = origType = tmp[1];
            //取出命名空间,通过“.”分割成数组
            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
            //如果定义了selector,决定选择哪个特殊事件api;如果没有selector,就使用type了
            type = ( selector ? special.delegateType : special.bindType ) || type;

            // Update special based on newly reset type
            //更新特殊事件的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;

                // Only use addEventListener if the special events handler returns false
                //如果特殊事件returnfalse,获取失败,就用addEventListener方法啦
                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;
                }
            }
            //将事件添加到事件列表handlers,delegate事件(委托事件)放到列表头部,其他事件放到尾部
            // Add to the element‘s handler list, delegates in front
            if ( selector ) {
                handlers.splice( handlers.delegateCount++, 0, handleObj );
            } else {
                handlers.push( handleObj );
            }

            // Keep track of which events have ever been used, for event optimization
            //跟踪事件已被使用,用于事件优化
            jQuery.event.global[ type ] = true;
        }

    },

jQuery.event.add函数以下功能:

// 通过内部缓存获取元素数据
            elemData = data_priv.get( elem );
  • 获取数据缓存。如果事件被注册过,用get方法从缓存中拿。如果缓存中没有,就创建一个用途elem映射的缓存区,这个处理主要是合并同个元素绑定多个事件的问题。就是把相同元素绑定的不同行为都合并到一个缓存里。
    // Make sure that the handler has a unique ID, used to find/remove it later
        //如果handler没有id,则给它定义一个id,确保每个handler都有一个id,用于未来查找和删除
        if ( !handler.guid ) {
            handler.guid = jQuery.guid++;
        }
  • 创建编号 添加编号的目的是用来寻找和删除事件,因为事件是存储在缓存里的,并没有与元素直接关联。
        // Init the element‘s event structure and main handler, if this is the first
        //如果是第一次创建,初始化元素的事件结构和主要handler
        // 如果缓存数据中没有events数据
        if ( !(events = elemData.events) ) {
            // 则初始化events
            events = elemData.events = {};
        }
         // 如果缓存数据中没有handle数据
        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
                // 丢弃jQuery.event.trigger第二次触发事件以及当一个页面被卸载后调用事件
                return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ?
                    jQuery.event.dispatch.apply( elem, arguments ) : undefined;
            };
        }
//eleData结构
elemData = {
       events:{}
       eventHandle:function(){}
}
  • 给缓存elemData增加属性 events是内部维护的事件队列,eventHandle 是作用到addeventlistener的回调函数。这里把回调函数设为jQuery.event.dispatch.apply( elem, arguments )。
  • 填充事件名与事件句柄 取出命名空间,事件类型等
  • 区分了特殊事件和普通事件。为特殊事件进行了组装。并引用了钩子处理特殊事件。
  • 引入了一个handlers事件处理list,把事件全部放到list里一个个执行,而且还区分了delegate事件和其他事件,delegate事件放在list头,其他事件放在list尾。并未delegate事件添加了delegateCount属性,委托次数。
  • 最后执行addeventlistener函数,callback函数是jQuery.event.dispatch.apply( elem, arguments )方法

总的来说就是:
1. 通过 add 把数据整理放到数据缓存中保存,通过 addEventListener 绑定事件
2. 触发事件执行 addEventListener 回调 dispatch 方法

jquery2.1.3没有做IE8以前的事件兼容,默认放弃了IE8以前的版本,没有做attachEvent事件的兼容处理。

jQuery.event.dispatch


// 4391行
dispatch: function( event ) {

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

        var i, j, ret, matched, handleObj,
            handlerQueue = [],
            //参数转数组
            args = slice.call( arguments ),
             // 从内部数据中查找该元素的对应事件处理器列表中的对应处理器,否则为空数组
            handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [],
            // 尝试将事件转成特殊事件
            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
        // 尝试使用特殊事件的preDispatch钩子来绑定事件,并在必要时退出
        if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
            return;
        }

        // Determine handlers
         // 组装事件处理包{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() ) {

                // 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).
                // 触发的事件必须满足其一:
                // 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();
                        }
                    }
                }
            }
        }

        // Call the postDispatch hook for the mapped type
        // 尝试通过special.postDispatch勾住这个映射关系,未来可以优化
        if ( special.postDispatch ) {
            special.postDispatch.call( this, event );
        }
       // 返回事件函数
        return event.result;
    },

这里首先就调用了 jQuery.event.fix( event )函数对事件做了兼容处理。

jQuery.event.fix

//4496行  fix函数以及fix函数用到的函数们
// Includes some event props shared by KeyEvent and MouseEvent
    //props 存储了原生事件对象 event 的通用属性
    props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),

    fixHooks: {},
    //keyHook.props 存储键盘事件的特有属性
    keyHooks: {
        props: "char charCode key keyCode".split(" "),
        filter: function( event, original ) {

            // Add which for key events
            if ( event.which == null ) {
                event.which = original.charCode != null ? original.charCode : original.keyCode;
            }

            return event;
        }
    },
    //mouseHooks.props 存储鼠标事件的特有属性。
    mouseHooks: {
        props: "button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),
        //keyHooks.filter 和 mouseHooks.filter 两个方法分别用于修改键盘和鼠标事件的属性兼容性问题,用于统一接口。
        filter: function( event, original ) {
            var eventDoc, doc, body,
                button = original.button;

            // Calculate pageX/Y if missing and clientX/Y available
            if ( event.pageX == null && original.clientX != null ) {
                eventDoc = event.target.ownerDocument || document;
                doc = eventDoc.documentElement;
                body = eventDoc.body;

                event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
                event.pageY = original.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );
            }

            // Add which for click: 1 === left; 2 === middle; 3 === right
            // Note: button is not normalized, so don‘t use it
            if ( !event.which && button !== undefined ) {
                event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
            }

            return event;
        }
    },
 //大家看过来啦,,fix函数开始了
    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,
            originalEvent = event,
            fixHook = this.fixHooks[ type ];
         //判断事件的类型,是mouse事件还是key事件
        if ( !fixHook ) {
            this.fixHooks[ type ] = fixHook =
                rmouseEvent.test( type ) ? this.mouseHooks :
                rkeyEvent.test( type ) ? this.keyHooks :
                {};
        }
        //实际上就是判断事件是mouse事件还是key事件,如果是mouse事件把props和mouse事件的props都加到事件的属性里
        copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;

        event = new jQuery.Event( originalEvent );
//jQuery 自己写了一个基于 native event 的 Event 对象,并且把 copy 数组中对应的属性从 native event 中复制到自己的 Event 对象中。
        i = copy.length;
        while ( i-- ) {
            prop = copy[ i ];
            event[ prop ] = originalEvent[ prop ];
        }

        // Support: Cordova 2.5 (WebKit) (#13255)
        // All events should have a target; Cordova deviceready doesn‘t
        if ( !event.target ) {
            event.target = document;
        }

        // Support: Safari 6.0+, Chrome<28
        // Target should not be a text node (#504, #13143)
        if ( event.target.nodeType === 3 ) {
            event.target = event.target.parentNode;
        }
        //在最后 jQuery 还不忘放一个钩子,调用 fixHook.fitler 方法用以纠正一些特定的 event 属性。例如 mouse event 中的 pageX,pageY,keyboard event中的 which,进一步修正事件对象属性的兼容问题。
        return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;
    },

fix函数实际上就是将js原生事件的通用属性重写了一遍,附加到jquery自己创建的事件对象上面了。
为什么将浏览器原生 Event 的属性赋值到新创建的 jQuery.Event 对象中去哪?jQuery要增加自己的处理机制,这样更灵活,而且还可以传递 data 数据,也就是用户自定义的数据。

fix函数总结:

1.将原生的事件对象 event 修正为一个新的可写 event 对象,并对该 event 的属性以及方法统一接口
2.该方法在内部调用了 jQuery.Event(event) 构造函数

回到depatch函数,调用fix函数之后

  • 尝试把事件变成special事件
  • 调用 jQuery.event.handlers.call( this, event, handlers );函数组装事件处理包的队列handlerQueue。这里是不同元素的队列。相同元素的事件队列见前面elemData。
  • 遍历事件处理包,满足条件的进行触发。这里对callback函数return false的处理是既阻止冒泡也阻止事件的进一步传播。

jQuery.event.handlers

//4450行
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 ) {

                // Don‘t process clicks on disabled elements (#6911, #8165, #11382, #11764)
                // 防止单机被禁用的元素时触发事件
                if ( cur.disabled !== true || event.type !== "click" ) {
                // 开始组装符合要求的事件处理对象
                    matches = [];
                     // 遍历所有事件处理对象,还记得吗,add方法里面,delegate类型的事件都放在handlers队列的最前面了
                    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 });
                    }
                }
            }
        }

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

        return handlerQueue;
    },

handlers函数有什么作用呢?
现在有一个问题。假设一段jquery代码是这样的。


<div id="aaron">
   <div id=‘test‘>
        <ul>
            <p>点击测试委托顺序</p>
        </ul>
   </div>
</div>
var ul = $(‘ul‘)
function show(data){
  ul.append(‘<li>‘+ data +‘</li>‘)
}

var aaron = $("#aaron")
//同一个元素上绑定不同的事件委托
aaron.on(‘mousedown‘,‘p‘,function(e){
    show(‘p‘)
   e.stopPropagation()
})
aaron.on(‘mousedown‘,‘ul‘,function(e){
    show(‘被阻止了‘)
})
$("#test").on(‘mousedown‘,function(){
  show(‘test‘)
})

运行结果:
text p

发现“被阻止了”没有打印出来。

arron有两个事件委托。一个是子孙元素p元素委托arron,另一个是ul元素委托。当p元素委托之后,callback函数里明确规定stopPropagation,这时ul元素绑定的事件不能触发。

handlers函数就是为了实现这个目标哒。

handlers有如下需求
- 根据冒泡的原理,不管事件添加的顺序如何,应该为elem上的所有事件依照触发顺序排出一个层次来,最里的委托优先级最高,最外的委托优先级最低,这样停止冒泡的需求得以实现。

handlers实现原理

  • 之前add方法对每个elem缓存了一个数据结构elemData,还有一个区分有没有事件委托的变量delegateCount。现在只需根据delegateCount判断元素有没有事件委托。有事件委托,则对elemData里面的事件队列做一个排序,按照委托的节点的DOM深度排序,深度越深,排序越靠前,深度越低,排序越靠后。
  • 即通过 target 与实际的事件绑定对象我们就可以划分一个区域段,通过递归获取每一个元素的 parentNode 节点,在每一个节点层上通过与委托节点的对比用来确定是不是委托的事件元素,这个就是委托的核心思路了。

还有一个special事件的一堆函数,下次再写喽。

以上是关于jQuery源码分析--event事件绑定(上)的主要内容,如果未能解决你的问题,请参考以下文章

jquery事件委托详解

jQuery 判断元素上是不是绑定了事件

Jquery 获取元素上绑定的事件

jQuery Direct and delegated events 直接事件与委托事件

事件的绑定和解绑 (jQuery)

jquery——获取dom元素身上的绑定事件的问题