jQuery事件绑定原理(1)

jQuery事件处理机制能帮我们处理那些问题?

  • 解决浏览器事件兼容问题
  • 可以在一个事件类型上添加多个事件处理函数,可以一次添加多个事件类型的事件处理函数
  • 提供了常用事件的便捷方法
  • 支持自定义事件
  • 扩展了组合事件
  • 提供了统一的事件封装、绑定、执行、销毁机制

jQuery事件绑定原理(1)_第1张图片

jQuery的事件绑定有多个方法可以调用,以click事件来举例:1. click方法 2. bind方法 3. delegate方法 4. on方法

    $('#foo').click(function(){ })

    $('#foo').bind('click',function(){ })

    $("foo").delegate("td", "click", function() { });

    $("foo").on("click", "td", function() { });

我们来看一下具体实现

click方式

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 ) {
        // 合并15种事件统一增加到jQuery.fn上
        // name参数为事件名称
        jQuery.fn[ name ] = function( data, fn ) {
            // 内部调用this.on / this.trigger
            return arguments.length > 0 ?
                this.on( name, null, data, fn ) :
                this.trigger( name );
        };
    });

bind方式

    bind: function( types, data, fn ) {
        return this.on( types, null, data, fn );
    },
    unbind: function( types, fn ) {
        return this.off( types, null, fn );
    },

同样调用的this.on/this.off

delegate方式

    delegate: function( selector, types, data, fn ) {
        return this.on( types, selector, data, fn );
    },
    undelegate: function( selector, types, fn ) {
        // ( namespace ) or ( selector, types [, fn] )
        return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
    }

同样调用的this.on/this.off

one方式

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

可见以上的接口只是修改了不同的传递参数,最后都交给on实现的

on方式

on的调用方式是.on( events, selector, data, handler(eventObject)),events是事件名,selector 是一个选择器字符串,用于过滤出被选中的元素中能触发事件的后代元素。data 是当一个事件被触发时,要传递给事件处理函数的参数。handler是事件被触发时,执行的函数。

    var body = $('body')
    body.on('click','p',function(){
        console.log(this)
    })
    //两种写法相同,都是事件委托,支持动态绑定(新添加的p元素也有回调处理函数)
    body.delegate('p', 'click', function(){
        console.log(this)
    })

用on或者delegate方法给body上绑定一个click事件,当点击的是p元素的时候才触发回调函数。这里使用on或者delegate都是采用事件委托(即把p元素上的点击事件处理委托给body)。
通过源码不难发现,on方法实质只完成一些参数调整的工作,而实际负责事件绑定的是其内部jQuery.event.add方法。

jQuery事件绑定原理(1)_第2张图片

针对事件处理,我们可以拆分2部分:一个事件预绑定期,一个事件执行期。事件底层的绑定接口无非就是用addEventListener处理的,所以我们直接定位到addEventListener下面

   if(elem.addEventListener){
      //采用冒泡
      elm.addEventListener(type, eventHandle, false);
   }

这里的eventHandle显然是重点 ,可想而知eventHandle不仅仅只是只是充当一个回调函数的角色,而是一个实现了EventListener接口的对象

    if ( !(eventHandle = elemData.handle) ) { //handle是实际绑定到elem中的事件处理函数
       eventHandle = elemData.handle = function( e ) {
           jQuery.event.dispatch.apply( eventHandle.elem, arguments );
    };

可见在eventHandle中并没有直接处理回调函数,而是转到jQuery.event.dispatch分派事件处理函数了。当绑定的时候有 selector 的时候(也就是事件委托),add 函数处理添加事件,而事件的执行,要靠 dispatch,比如我们上面的例子,在 $('body').on('click','p',fn),我们点击body上的所有元素,会被监听,dispatch 函数是会执行的,但是 fn 不执行,除非我们点击p元素。
那么这里有个问题,jQuery.event.dispatch仅仅只是传入eventHandle.elem,arguments , 就是body元素与事件对象。事件回调的句柄并没有传递过去,后面的代码如何关联?
我们从开头来理清下jQuery.event.add代码结构,适当的跳过这个环节中不能理解的代码,jQuery从1.2.3版本引入数据缓存系统,DOM元素和事件要建立关系,最原始的方法是在DOM元素上绑定事件。jQuery为了不破坏DOM树结构,通过缓存的方式保存事件。所以jQuery并没有将事件处理函数直接绑定到DOM元素上,而是通过.data存储在缓存.cache上,elemData是这个体系的核心。

    add: function( elem, types, handler, data, selector ) {
        var handleObjIn, eventHandle, tmp,
        events, t, handleObj,
        special, handlers, type, namespaces, origType,
        elemData = data_priv.get( elem ); //存储事件句柄对象,elem元素的句柄对象

        if ( !handler.guid ) {
              handler.guid = jQuery.guid++; //为每一个事件的句柄给一个标示,添加id的目的是用来寻找或者删除handler,因为这个是缓存在缓存对象上的,没有直接跟元素节点发生关联
        }

        if ( !(events = elemData.events) ) {
             events = elemData.events = {}; //events是jQuery内部维护的事件列队
        }
        if ( !(eventHandle = elemData.handle) ) { //handle是实际绑定到elem中的事件处理函数
            eventHandle = elemData.handle = function( e ) {
            jQuery.event.dispatch.apply( eventHandle.elem, arguments );
        };
        eventHandle.elem = elem;
        //事件可能是通过空格键分隔的字符串,所以将其变成字符串数组
        types = ( types || "" ).match( core_rnotwhite ) || [""];
        t = types.length;
        while ( t-- ) {
            // 这里把handleObj叫做事件处理对象,扩展一些来着handleObjIn的属性
            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;

                if ( elem.addEventListener ) {
                     elem.addEventListener( type, eventHandle, false );
                }
            }

            // 将事件处理对象推入处理列表,姑且定义为事件处理对象包
            if ( selector ) {
                handlers.splice( handlers.delegateCount++, 0, handleObj );
            } else {
                handlers.push( handleObj );
            }
            // 表示事件曾经使用过,用于事件优化
            jQuery.event.global[ type ] = true;
        }
        // 设置为null避免IE中循环引用导致的内存泄露
        elem = null;
    }

我们来看一下截图了解一下events:handleObj对象

jQuery事件绑定原理(1)_第3张图片

在elemData中有两个重要的属性,一个是events,是jQuery内部维护的事件列队,一个是handle,是实际绑定到elem中的事件处理函数。
这里有一个很重要的地方,事件名称可以添加指定的event namespaces(命名空间) 来简化删除或触发事件。例如,click.myPlugin.simple为 click 事件同时定义了两个命名空间 myPlugin 和 simple。通过上述方法绑定的 click 事件处理,可以用.off(“click.myPlugin”) 或 .off(“click.simple”)删除绑定到相应元素的Click事件处理程序,而不会干扰其他绑定在该元素上的“click(点击)” 事件。命名空间类似CSS类,因为它们是不分层次的;只需要有一个名字相匹配即可。以下划线开头的名字空间是供 jQuery 使用的。
什么时候要用到自定义函数?有些浏览器并不兼容某类型的事件,如IE6~8不支持hashchange事件,你无法通过jQuery(window).bind(‘hashchange’, callback)来绑定这个事件,这个时候你就可以通过jQuery自定义事件接口来模拟这个事件,做到跨浏览器兼容。jQuery(elem).bind(type, callbakc)实际上是映射到 jQuery.event.add(elem, types, handler, data)这个方法,每一个类型的事件会初始化一次事件处理器,而传入的回调函数会以数组的方式缓存起来,当事件触发的时候处理器将依次执行这个数组。
jQuery.event.add方法在第一次初始化处理器的时候会检查是否为自定义事件,如果存在则将会把控制权限交给自定义事件的事件初始化函数,同样事件卸载的jQuery.event.remove方法在删除处理器前也会检查。

总结

jQuery.event.dispatch.apply( eventHandle.elem, arguments ) 方法中没有传递回调对象是因为回调的句柄被关联到了elemData,也就是内部数据缓存中了。
不难得出jQuery的事件绑定机制:

  • jQuery对每一个elem中的每一种事件,只会绑定一次事件处理函数(绑定这个elemData.handle)
  • 而这个elemData.handle实际只做一件事,就是把event丢到jQuery内部的事件分发程序jQuery.event.dispatch.apply(eventHandle.elem, arguments )。具体这个函数可以参考下一篇博文jQuery事件绑定原理(2)
  • 而不同的事件绑定,具体是由jQuery内部维护的事件列队来区分(就是那个elemData.events)
  • 在elemData中获取到events和handle之后,接下来就需要知道这次绑定的是什么事件了。

你可能感兴趣的:(jQuery)