欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

jQuery 源码分析(十六) 事件系统模块 底层方法 详解

程序员文章站 2022-07-01 13:42:30
jQuery事件系统并没有将事件监听函数直接绑定到DOM元素上,而是基于数据缓存模块来管理监听函数的,事件模块代码有点多,我把它分为了三个部分:分底层方法、实例方法和便捷方法、ready事件来讲,好理解一点。 jQuery的事件分为普通事件和代理事件: 普通事件 ;当我们再div上定义一个click ......

jquery事件系统并没有将事件监听函数直接绑定到dom元素上,而是基于数据缓存模块来管理监听函数的,事件模块代码有点多,我把它分为了三个部分:分底层方法、实例方法和便捷方法、ready事件来讲,好理解一点。

jquery的事件分为普通事件和代理事件:

  • 普通事件  ;当我们再div上定义一个click事件,此时如果点击div或按钮都会触发该普通事件,这是由于冒泡的缘故
  • 代理事件  ;当我们在div上定义一个代理事件,且selector设置为button时,我们点击div将不会触发该事件,只有点击了这个按钮才会触发这个代理事件

事件系统模块的底层方法如下:

  • $.event.add(elem,types,handler,data,selector)  ;绑定一个或多个类型的事件监听函数,参数如下:
    • elem        ;操作的元素
    • types        ;绑定的事件类型,多个事件类型之间用空格隔开。
    • handler    ;待绑定的事件监听函数,也可以是一个自定义的监听对象。
    • data        ;自定义数据。
    • selector    ;选择器表达式字符串,用于绑定代理事件。    ;
  • $.event.remove(elem, types, handler, selector, mappedtypes)  ;移除dom元素上绑定的一个或多个类型的事件监听函数,参数同$.event.add()。如果只传入一个elem元素则移除该元素上的所有事件。
  • $.event.trigger(type,data,elem,onlyhandlers)    ;手动触发事件,执行绑定的事件监听函数和默认行为,并且会模拟冒泡过程。参数如下:
    • type            ;事件字符串
    • data                 ;传递给响应函数的数据
    • elem            ;触发该事件的dom对象
    • onlyhandlers    ;是否只触发elem元素对应的事件监听函数,而不冒泡

常用的就是以上三个吧,其它还有$.event.global(记录绑定过的事件)、$.event.removeevent(用于删除事件)等

先举栗子前先先简单说一下jquery里的事件的分类,jquery的事件分为普通事件和代理事件:

  • 普通事件  ;直接绑定在这个元素的某个事件类型上,当在该元素上触发了这个事件时,则执行该事件
  • 代理事件  ;当事件直接发生在代理元素上时,监听函数不会执行,只有当事件从后代元素冒泡到代理元素上时,才会用参数selector匹配冒泡路上的后代元素,然后用匹配成功的后代元素作为上下文去执行监听函数。

这样说可能理解不了,举个栗子,我们定义两个dom元素先

jQuery 源码分析(十六)  事件系统模块 底层方法 详解

这里我们定义了一个div,内部含有一个子节点button,渲染如下:

jQuery 源码分析(十六)  事件系统模块 底层方法 详解

举个栗子:

writer by:大沙漠 qq:22969969

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>document</title>
    <script src="http://libs.baidu.com/jquery/1.7.1/jquery.min.js"></script>
    <style>div{width: 200px;padding-top:50px;height: 150px;background: #ced;}div button{margin:0 auto;display: block;}</style>
</head>
<body>
    <div>        
        <button id="button">按钮1</button>    
    </div>
    <script>
        let div = document.getelementsbytagname('div')[0],
            btn = document.getelementsbytagname('button')[0];

        $.event.add(div,'click',()=>console.log('div普通单击事件'));                //给div绑定一个click事件
        $.event.add(div,'click',()=>console.log('d1代理事件'),null,'button');    //给div绑定一个代理事件,监听对象为button
    </script>
</body>
</html>

渲染如下:

jQuery 源码分析(十六)  事件系统模块 底层方法 详解

 我们给div绑定了一个普通事件(类型为click),还有一个代理事件(代理的元素是button),当我们点击div元素时将触发普通事件,如下:

jQuery 源码分析(十六)  事件系统模块 底层方法 详解

代理事件并没有被触发,当我们点击按钮1时将会同时触发普通事件和代理事件:

jQuery 源码分析(十六)  事件系统模块 底层方法 详解

这里的代理事件是在jquery内部实现的,而普通事件是因为原生的冒泡的事件流所产生的,

 

源码分析


jquery内部的事件绑定也是通过原生的addeventlistener或attachevent来实现的,不过jquery对这个过程做了优化,它不只是仅仅的调用该api实现绑定,用jquery绑定事件时,在同一个dom元素上的绑定的任何事件,其实最后绑定的都是同一个函数,该函数只有几行diamagnetic,最后又会调用jquery.event.dispatch去进行事件的分发,并执行事件监听函数。

上面说了事件系统模块是基于数据缓存模块来管理监听函数的,我们通过jquery绑定的事件都保存到了$. cache里对应的dom元素的数据缓存对象上(有疑问的可以看下数据缓存模块,前面介绍过了),比如上面的栗子,我们打印一下$.cache可以看到绑定的信息:

jQuery 源码分析(十六)  事件系统模块 底层方法 详解

每个dom元素在内部数据缓存对上有两个属性是和事件有关的:

  • events  ;属性是一个对象,其中存储了该dom元素的所有事件,该对象的下的每个元素的元素名是事件类型,值是一个数组,是封装了监听函数的handleobject集合
  • handle  ;dom元素的主监听函数,负责分发事件和执行监听函数,对于一个dom元素,jquery事件系统只会为之分配一个主监听函数,所有类型的事件都被绑定到这个主监听函数

 好了,现在来说一下源码实现,$.event.add的源码实现如下:

jquery.event = {
    add: function( elem, types, handler, data, selector ) {        //绑定一个或多个类型的事件监听函数

        var elemdata, eventhandle, events,
            t, tns, type, namespaces, handleobj,
            handleobjin, quick, handlers, special;

        // don't attach events to nodata or text/comment nodes (allow plain objects tho)
        if ( elem.nodetype === 3 || elem.nodetype === 8 || !types || !handler || !(elemdata = jquery._data( elem )) ) {        //排除文本节点(浏览器不会在文本节点上触发事件)、注释节点(没有意义)、参数不完整的情况
            return;
        }

        // caller can pass in an object of custom data in lieu of the handler
        if ( handler.handler ) {                                    //如果参数handle是自定义监听对象,其中的属性价会被设置到后面所创建的新监听对象上。函数clonecopyevent(src,dest)将调用该方法
            handleobjin = handler;
            handler = handleobjin.handler;
        }

        // make sure that the handler has a unique id, used to find/remove it later
        if ( !handler.guid ) {                                        //如果函数handler没有guid
            handler.guid = jquery.guid++;                                //则为它分配一个唯一标识guid,在移除监听函数时,将通过这个唯一标识来匹配监听函数。
        }

        // init the element's event structure and main handler, if this is the first
        events = elemdata.events;                                    //尝试取出事件缓存对象
        if ( !events ) {                                            //如果不存在,表示从未在当前元素上(通过jquery事件方法)绑定过事件,
            elemdata.events = events = {};                                //则把它初始化为一个空对象。events对象保存了存放当前元素关联的所有监听函数。
        }    
        eventhandle = elemdata.handle;                                //尝试取出主监听函数handle(event)
        if ( !eventhandle ) {
            elemdata.handle = eventhandle = function( e ) {            //如果不存在,表示从未在当前元素上(jquery事件方法)绑定过事件,则初始化一个主监听函数,并把它存储到事件缓存对象的handle属性上,这是事件触发时真正执行的函数
                // discard the second event of a jquery.event.trigger() and
                // when an event is called after a page has unloaded
                return typeof jquery !== "undefined" && (!e || jquery.event.triggered !== e.type) ?
                    jquery.event.dispatch.apply( eventhandle.elem, arguments ) :
                    undefined;
            };
            // add elem as a property of the handle fn to prevent a memory leak with ie non-native events
            eventhandle.elem = elem;
        }

        // handle multiple events separated by a space
        // jquery(...).bind("mouseover mouseout", fn);
        types = jquery.trim( hoverhack(types) ).split( " " );        //先调用hoverhack()函数修正hover.namespace类型的事件,再调用split把types用空格进行分隔,转换为一个数组
        for ( t = 0; t < types.length; t++ ) {                        //遍历需要绑定的每个事件类型,逐个绑定事件

            tns = rtypenamespace.exec( types[t] ) || [];                //正则rtypenamespace用于解析事件类型和命名空间,执行后tns[1]是事件类型,tns[2]是一个或多个命名空间,用.分隔
            type = tns[1];                                                //type就是事件类型
            namespaces = ( tns[2] || "" ).split( "." ).sort();

            // 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
            type = ( selector ? special.delegatetype : special.bindtype ) || type;

            // update special based on newly reset type
            special = jquery.event.special[ type ] || {};

            // handleobj is passed to all event handlers
            handleobj = jquery.extend({                                    //把监听函数封装为监听对象,用来支持事件模拟、自定义事件数据等。
                type: type,                                                    //实际使用的事件类型,不包含命名空间,可能被修改过
                origtype: tns[1],                                            //原始事件类型,不包含命名空件,未经过修正
                data: data,                                                    //排序后的命名空间,如果传入的事件类型是'click.a.c.b',那么namespace就是a.b.c
                handler: handler,                                            //传入的监听函数
                guid: handler.guid,                                            //分配给监听函数的唯一标识guid
                selector: selector,                                            //自定义的事件数据
                quick: quickparse( selector ),                                //入的事件代理选择器表达式,当代理事件被触发时,用该属性过滤代理元素的后代元素。
                namespace: namespaces.join(".")
            }, handleobjin );

            // init the event handler queue if we're the first
            handlers = events[ type ];                                    //尝试取出事件类型type对应的监听对象数组handlers,其中存放了已绑定的监听对象。
            if ( !handlers ) {                                            //如果type事件类型的数组不存在,则进行绑定操作
                handlers = events[ type ] = [];                                //把监听对象数组的handlers初始化为一个空数组。
                handlers.delegatecount = 0;                                    //初始化handlers.delegatecount为0,表示代理事件的个数为0 

                // only use addeventlistener/attachevent if the special events handler returns false
                if ( !special.setup || special.setup.call( elem, data, namespaces, eventhandle ) === false ) {    //绑定主监听函数 优先调用修正对象的修正方法setup()
                    // bind the global event handler to the element
                    if ( elem.addeventlistener ) {                            //如果浏览器支持addeventlistener()方法
                        elem.addeventlistener( type, eventhandle, false );        //调用原生方法addeventlistener()绑定主监听函数,以冒泡流的方式。

                    } else if ( elem.attachevent ) {                        //如果没有addeventlistener()
                        elem.attachevent( "on" + type, eventhandle );            //则调用attachevent()方法绑定主监听函数,ie8及更早的浏览器只支持冒泡
                    }
                }
            }

            if ( special.add ) {                                        //如果修正对象有修正方法add()
                special.add.call( elem, handleobj );                        //则先调用修正方法add()绑定监听函数

                if ( !handleobj.handler.guid ) {
                    handleobj.handler.guid = handler.guid;
                }
            }

            // add to the element's handler list, delegates in front
            if ( selector ) {                                            //如果传入了selector参数,则绑定的是代理事件
                handlers.splice( handlers.delegatecount++, 0, handleobj );    //把代理监听对象插入属性handlers.delegatecount所指定的位置。每次插入代理监听对象后,监听对象数组的属性handlers.delegatecount自动加1,以指示下一个代理监听对象的插入位置。
            } else {                                                     //未传入selector参数情况下,则是普通的事件绑定
                handlers.push( handleobj );                                    //把监听对象插入末尾
            }

            // keep track of which events have ever been used, for event optimization
            jquery.event.global[ type ] = true;                            //记录绑定过的事件类型
        }

        // nullify elem to prevent memory leaks in ie
        elem = null;                                                    //解除参数elem对dom元素的引用,以避免内存泄漏(ie)
    },
    /*略*/
}

 $.event.add会通过addeventlistener或attachevent去绑定事件,当事件被触发时就会执行我们绑定的事件,也就是上面的elemdata.handle函数,该函数会执行jquery.event.dispatch函数,jquery.event.dispatch就是用于分发事件的,如下:

 

jquery.event = {
    dispatch: function( event ) {        //分发事件,执行事件监听函数

        // make a writable jquery.event from the native event object
        event = jquery.event.fix( event || window.event );                //调用jquery.event.fix(event)把原生事件对象封装为jquery事件对象。

        var handlers = ( (jquery._data( this, "events" ) || {} )[ event.type ] || []),        //取出this元素当前事件类型对应的函数列表。
            delegatecount = handlers.delegatecount,                                            //代理监听对象个事
            args = [].slice.call( arguments, 0 ),                                            //把参数arguments转换为真正的数组
            run_all = !event.exclusive && !event.namespace,
            handlerqueue = [],
            i, j, cur, jqcur, ret, selmatch, matched, matches, handleobj, sel, related;

        // use the fix-ed jquery.event rather than the (read-only) native event
        args[0] = event;                                                                    //将event保存为args[0],监听函数执行时这是作为第一个参数传入的
        event.delegatetarget = this;                                                        //当前的dom对象,等于event.currenttarget属性的值

        // determine handlers that should run if there are delegated events
        // avoid disabled elements in ie (#6911) and non-left-click bubbling in firefox (#3861)
        if ( delegatecount && !event.target.disabled && !(event.button && event.type === "click") ) {    //如果当前对象绑定了代理事件,且目标对象target没有禁用  !(event.button && event.type === "click")的意思是:是鼠标单击时

            // pregenerate a single jquery object for reuse with .is()
            jqcur = jquery(this);                                                                            //用当前元素构造一个jquery对象,以便在后面的代码中复用它的方法.is(selector)
            jqcur.context = this.ownerdocument || this;                                                        //上下文

            for ( cur = event.target; cur != this; cur = cur.parentnode || this ) {                            //从事件目标开始,再依次到父节点,一直到当前目标为止,遍历从触发事件的元素到代理元素这条路径上的所有后代元素。
                selmatch = {};                                                                                    //重置selmatch为空对象
                matches = [];                                                                                    //重置matches为空数组
                jqcur[0] = cur;                                                                                    //将jqcur绑定到每一个后代元素
                for ( i = 0; i < delegatecount; i++ ) {                                                            //遍历所有的代理监听对象数组。
                    handleobj = handlers[ i ];                                                                        //某个代理监听对象。
                    sel = handleobj.selector;                                                                        //当前代理监听对象的selector属性    

                    if ( selmatch[ sel ] === undefined ) {                                                            //如果绑定的事件对应的选择器在selmatch中不存在,则进行检查,看是否符合要求
                        selmatch[ sel ] = (
                            handleobj.quick ? quickis( cur, handleobj.quick ) : jqcur.is( sel )
                        );
                    }
                    if ( selmatch[ sel ] ) {                                                                        //当后代元素与代理监听对象的选择器表达式匹配时
                        matches.push( handleobj );                                                                    //把代理监听对象放入数组matches中。
                    }
                }
                if ( matches.length ) {
                    handlerqueue.push({ elem: cur, matches: matches });                                            //最后把某个后代元素匹配的所有代理监听对象代理放到数组handlerqueue里。
                }
            }
        }

        // add the remaining (directly-bound) handlers
        if ( handlers.length > delegatecount ) {                                                //如果监听对象数组handlers的长度大于代理监听对象的位置计数器delegatecount,则表示为当前元素绑定了普通事件
            handlerqueue.push({ elem: this, matches: handlers.slice( delegatecount ) });            //把这些监听对象也放入待执行队列handlerqueue中
        }

        // run delegates first; they may want to stop propagation beneath us
        for ( i = 0; i < handlerqueue.length && !event.ispropagationstopped(); i++ ) {            //执行数组handlerqueue中的所有函数。遍历待执行队列handlerqueue,如果某个元素的监听函数调用了方法stoppropagation()则终止for循环
            matched = handlerqueue[ i ];                                                            //matched是数组handlerqueue中的一个对象
            event.currenttarget = matched.elem;                                                        //把当前正在执行监听函数的元素赋值给事件属性event.currenttarget。这样在事件处理函数内,this等于绑定的代理函数

            for ( j = 0; j < matched.matches.length && !event.isimmediatepropagationstopped(); j++ ) {    //遍历元素定对应的监听对象数组,并执行监听对象中的监听函数
                handleobj = matched.matches[ j ];                                                            //handleobj是一个具体的监听对象handleobj

                // triggered event must either 1) be non-exclusive and have no namespace, or
                // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
                if ( run_all || (!event.namespace && !handleobj.namespace) || event.namespace_re && event.namespace_re.test( handleobj.namespace ) ) {

                    event.data = handleobj.data;                                                        //把监听对象的属性handleobj.data赋值给jquery事件对象
                    event.handleobj = handleobj;                                                        //把监听对象赋值给jquery事件对象event.handleobj上,这样监听函数就可以通过该属性访问监听对象上的诸多属性

                    ret = ( (jquery.event.special[ handleobj.origtype ] || {}).handle || handleobj.handler )    //优先调用修正对象的修正方法special.handle(),
                            .apply( matched.elem, args );

                    if ( ret !== undefined ) {                                                            //如果函数有返回值
                        event.result = ret;
                        if ( ret === false ) {                                                            //如果监听对象的返回值是false
                            event.preventdefault();                                                            //调用preventdefault()方法阻止默认行为
                            event.stoppropagation();                                                           //stoppropagation()方法停止事件传播
                        }
                    }
                }
            }
        }

        return event.result;
    },
    /*略*/
}

jquery.event.fix()会把原生事件修正为jquery事件对象并返回,修正不兼容属性,就是自定义一个对象,保存该事件的信息,比如:类型、创建的时间、原生事件对象等,有兴趣的可以调试一下。