惟有于技术中才能明得真相

jQuery源代码阅读(五):Event

jQuery有着灵活的事件处理机制,可以自定义事件,支持事件的命名空间,可以手动地触发事件。这里不会讲怎么使用jQuery的事件,而是要分析它的内部实现。在详细分析源代码之前,我要先说明一下Query的事件处理的内部数据结构。对于每个注册了事件的元素,它都会包含一个events的属性,它包含了跟这个元素绑定的所有事件。从实现上说,events并不是元素的一个属性,它的一个数据项(通过调用data方法),之所以要这样做是为了防止循环引用,从而导致内存泄露,参见《jQuery源代码阅读(四):data方法》,两者从效果上并没有什么区别,这里为了说明简单将events看成是元素的一个属性,在看源代码时却需要注意。events并不一个扁平的数组,它继续按事件类型(如click, focus, mouseover, load等)划分,对每个事件类型,它又包含事件处理器,每个事件处理器有个type属性,表示该事件处理器所在命名空间(namespace),如果没有命名空间,那么其值就是一个空字符串。画成图,就下面这个样子:



我们知道,要让浏览器触发事件,需要调用addEventListener(对Firefox等)或attachEvent(IE)方法。对上面的结构,要使浏览器触发事件,一个简单方法,就是对每种类型的事件包含的handles中每个handle调用addEventListener或attachEvent方法,也就是说,对每种类型的事件可能注册多个事件处理器,这种做法简单直观,并且不需要额外的处理。但jQuery内部却不是这样实现的,它对每种类型的事件只注册一个事件处理器,然后再由这个事件处理器依次调用和这个事件相关联的每个handle,这种做法看起来复杂,却要灵活一些,jQuery藉此来实现带命名空间的事件。在内部,每种类型的事件注册的事件处理器都是一样的,保存为Element的handle属性(同样地,它其实只是一个数据对象)。

明白了这些,就可以来看jQuery的源代码了。先来看bind方法,所有添加事件的方法最终都是通过这个方法来调用的,如click(func)实际上调用的是bind("click", func)。

2895     bind: function( type, data, fn ) {
2896         return type == "unload" ? this.one(type, data, fn) : this.each(function(){
2897             jQuery.event.add( this, type, fn || data, fn && data );
2898         });
2899     },


从上面可以看到,bind方法调用是对每个元素调用jQuery.event.add函数,unload事件有些特殊,它只有调用一次,但调用的也是jQuery.event.add函数。接着看jQuery.event.add函数,先看前一部分:

2433 jQuery.event = {
2434
2435     // Bind an event to an element
2436     // Original by Dean Edwards
2437     add: function(elem, types, handler, data) {
2438         if ( elem.nodeType == 3 || elem.nodeType == 8 )
2439             return;
2440
2441         // For whatever reason, IE has trouble passing the window object
2442         // around, causing it to be cloned in the process
2443         if ( elem.setInterval && elem != window )
2444             elem = window;
2445
2446         // Make sure that the function being executed has a unique ID
2447         if ( !handler.guid )
2448             handler.guid = this.guid++;
2449
2450         // if data is passed, bind to handler
2451         if ( data !== undefined ) {
2452             // Create temporary function pointer to original handler
2453             var fn = handler;
2454
2455             // Create unique handler function, wrapped around original handler
2456             handler = this.proxy( fn );
2457
2458             // Store data in unique handler
2459             handler.data = data;
2460         }


我们看到add函数接收4个参数,第1个是产生事件的元素,第2个参数是事件名,可以包含多个事件,它们以空格分隔,每个事件可以包含命名空间,例如"click.ns1 focus.ns2"包含两个事件,分别是click和focus,click事件的命名空间是ns1,focus事件的命名空间是ns2。第3个参数是事件处理器,它是一个回调函数,接收两个参数,第1个是封装此次事件信息的Event对象,第2个是data对象,也就是add函数的第4个参数。add函数刚开始主要处理的是handler参数,设置它的guid和data属性,guid用于在jQuery内部惟一标识一个事件处理器,凡是guid相同的handler都被jQuery认为是相同的,在jQuery要移除一个handler时,依据的也只是它的guid属性。在2456行,将参数传进来的handler进行了一次代理返回一个新的handler,在内部,这个新handler只是简单地转发调用到旧的handler。下面是proxy函数的实现:

2761     proxy: function( fn, proxy ){
2762         proxy = proxy || function(){ return fn.apply(this, arguments); };
2763         // Set the guid of unique handler to the same of original handler, so it can be removed
2764         proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++;
2765         // So proxy can be declared as an argument
2766         return proxy;
2767     },


可以看到proxy除了转发调用以外,还给fn设置了相同的guid,这样以后可以用fn来移除事件。至于为什么要代理一下,我猜想大概是因为不想覆盖fn原有的属性(尽管它可能覆盖fn原有的guid属性)。

继续jQuery.event.add接下来的实现:

2463         var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}),
2464             handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function(){
2465                 // Handle the second event of a trigger and when
2466                 // an event is called after a page has unloaded
2467                 return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
2468                     jQuery.event.handle.apply(arguments.callee.elem, arguments) :
2469                     undefined;
2470             });
2471         // Add elem as a property of the handle function
2472         // This is to prevent a memory leak with non-native
2473         // event in IE.
2474         handle.elem = elem;


第2463行取出的每个元素的events属性,它包含了所有的事件处理器,新的handler也将要添加到events中去(在接下来的代码中)。第2464-2470行取得元素的handle属性,如果不存在,则设置一个新的,前面说过,这个handle才是真正会被注册到浏览器的事件处理器,它的实现委托给jQuery.event.handle函数(2468行)。第2468行中arguments.callee.elem其实就是jQuery.event.add函数中的elem参数,argument.callee就是handle,它的elem属性在2474行设置,那么为什么不直接写elem呢?根据第2471-2473的注释,说是为了防止在IE下的内存泄露,这个我不理解,反正这也不是我的关注点,就懒得管了。先来看下jQuery.event.handle函数:

2665     handle: function(event{
2666         // returned undefined or false
2667         var all, handlers;
2668
2669         event = arguments[0] = jQuery.event.fix( event || window.event );
2670         event.currentTarget = this;
2671         
2672         // Namespaced event handlers
2673         var namespaces = event.type.split(".");
2674         event.type = namespaces.shift();
2675
2676         // Cache this now, all = true means, any handler
2677         all = !namespaces.length && !event.exclusive;
2678         
2679         var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)");
2680
2681         handlers = ( jQuery.data(this, "events") || {} )[event.type];
2682
2683         for ( var j in handlers ) {
2684             var handler = handlers[j];
2685
2686             // Filter the functions by class
2687             if ( all || namespace.test(handler.type) ) {
2688                 // Pass in a reference to the handler function itself
2689                 // So that we can later remove it
2690                 event.handler = handler;
2691                 event.data = handler.data;
2692
2693                 var ret = handler.apply(this, arguments);
2694
2695                 if( ret !== undefined ){
2696                     event.result = ret;
2697                     if ( ret === false ) {
2698                         event.preventDefault();
2699                         event.stopPropagation();
2700                     }
2701                 }
2702
2703                 ifevent.isImmediatePropagationStopped() )
2704                     break;
2705
2706             }
2707         }
2708     },


根据开头的说明,这个handle的功能应该是依次调用events和当前事件类型相关的所有handlers,当然命名空间也得匹配,前面也说过,每个handler的命名空间保存在它的type属性中。牢牢记住这点,就不难理解handle函数了。第2669行调用fix函数规范化event对象,这主要是考虑Firefox等浏览器和IE浏览器的有些属性名称不一样,例如标准浏览器器中的事件目的元素为event.target,而在IE中为event.srcElement,fix函数的目的就是将它统一为target。关于标准浏览器和IE的事件对象的不同可以参见《JavaScript高级程序设计》。第2672-2679行是为了取得事件的类型和命名空间,命名是不分顺序的,ns1.ns2和ns1.ns2都代表命名空间ns1和ns2,所以命名空间需要正规化处理,最简单的方法就是先排序然后再连接(2697行)。第2681行取得元素的events属性,并根据事件类型取得所有的handlers。第2683-2707行就是依次调用每个命名空间匹配的handler。第2697-2700行表明,handler的返回值false,相当于对事件调用preventDefault和stopPropagation。第2703到2704行表示,当在事件上调用了stopImmediatePropagation时,会中断事件链的处理。注意stopPropagation和stopImmediatePropagation的区别,stopPropagation是阻止事件在父元素上同一事件的触发,而stopImmediatePropagation是为了阻止当前元素的下一个事件处理器的触发。在第2677行要注意event的exclusive属性,当它为false时(默认值)表示,当触发事件的命名空间为空时,它会调用该事件类型下的所有处理器,不管其命名空间是什么,当exclusive属性为true时,当触发事件命名空间为空时,则只会调用该事件类型下命名空间也为空的事件处理器。当调用trigger和triggerHandler时通过将事件类型名字后面加上"!"就表示这个事件是exclusive的。可以用下面的例子来说明,第一次调用trigger的事件不是exclusive,它会触发两个handler,而第二次调用trigger的事件是exclusive,它只会触发不带命名空间的handler。

/>>> $(document).bind("click", function() { console.log("click"); })
/>>> $(document).bind("click.ns", function() { console.log("click.ns"); })
/>>> $(document).trigger("click")
click
click.ns
/>>> $(document).trigger("click!")
click


让我们继续来看jQuery.event.add函数:

2476         // Handle multiple events separated by a space
2477         // jQuery(...).bind("mouseover mouseout", fn);
2478         jQuery.each(types.split(/\s+/), function(index, type) {
2479             // Namespaced event handlers
2480             var namespaces = type.split(".");
2481             type = namespaces.shift();
2482             handler.type = namespaces.slice().sort().join(".");
2483
2484             // Get the current list of functions bound to this event
2485             var handlers = events[type];
2486             
2487             if ( jQuery.event.specialAll[type] )
2488                 jQuery.event.specialAll[type].setup.call(elem, data, namespaces);
2489
2490             // Init the event handler queue
2491             if (!handlers) {
2492                 handlers = events[type] = {};
2493
2494                 // Check for a special event handler
2495                 // Only use addEventListener/attachEvent if the special
2496                 // events handler returns false
2497                 if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem, data, namespaces) === false ) {
2498                     // Bind the global event handler to the element
2499                     if (elem.addEventListener)
2500                         elem.addEventListener(type, handle, false);
2501                     else if (elem.attachEvent)
2502                         elem.attachEvent("on" + type, handle);
2503                 }
2504             }
2505
2506             // Add the function to the element's handler list
2507             handlers[handler.guid] = handler;
2508
2509             // Keep track of which events have been used, for global triggering
2510             jQuery.event.global[type] = true;
2511         });
2512
2513         // Nullify elem to prevent memory leaks in IE
2514         elem = null;
2515     },
2516
2517     guid: 1,
2518     global: {},


如果忽略一些细节,接下来的代码的事情就是将handler添加到events对应事件类型的handlers中去。第2480-2482行获得事件的类型及命名空间,并将其命名空间赋给handler的type属性。第2485行获得events对应事件类型的handlers,第2507行将handler添加到handlers中去。第2499-2502行将handle事件处理器注册到浏览器中,正如我前面所说,注册到浏览器中的事件处理器是handle,它再负责调用多个handlers。第2510行将所有注册的事件类型保存到jQuery.event.global中,这是为了触发全局事件,调用jQuery.event.trigger时不指定元素即会触发全局事件,它会触发注册到所有元素上的事件处理器。jQuery有一些钩子函数可以用来扩展其事件机制,那就是jQuery.event.specialAll和jQuery.event.special,对每种事件类型可以为它定义setup和teardown方法。第2487-2488行如果事件类型定义了specialAll处理器则会先调用这个处理器,在2497行说明如果事件类型定义了special处理器则会先调用这个处理器,两者的区别在于specialAll处理器每次调用jQuery.event.add函数时都会调用这个处理器,而special处理器仅在第一次注册该类型的事件时才会调用这个处理器。在内部,jQuery定义了live事件的specialAll处理器来实现live handler,定义了mouseenter, mouseleave事件的special处理器来实现mouseenter, mouseleave事件。

和add函数对应的是remove方法,它的实现基本上是将事件handler从events中移除,没有什么特别复杂的,这里就不讲了。jQuery还可以手动触发事件,这就需要调用jQuery.event.trigger函数,jQuery.fn.trigger和triggerHandler方法实际调用的就是jQuery.event.trigger函数,它的实现如下:

2591     trigger: functionevent, data, elem, bubbling ) {

2592         // Event object or event type

2593         var type = event.type || event;

2594

2595         if( !bubbling ){

2596             event = typeof event === "object" ?

2597                 // jQuery.Event object

2598                 event[expando] ? event :

2599                 // Object literal

2600                 jQuery.extend( jQuery.Event(type), event ) :

2601                 // Just the event type (string)

2602                 jQuery.Event(type);

2603

2604             if ( type.indexOf("!") >= 0 ) {

2605                 event.type = type = type.slice(0, -1);

2606                 event.exclusive = true;

2607             }

2608

2609             // Handle a global trigger

2610             if ( !elem ) {

2611                 // Don't bubble custom events when global (to avoid too much overhead)

2612                 event.stopPropagation();

2613                 // Only trigger if we've ever bound an event for it

2614                 if ( this.global[type] )

2615                     jQuery.each( jQuery.cache, function(){

2616                         if ( this.events && this.events[type] )

2617                             jQuery.event.trigger( event, data, this.handle.elem );

2618                     });

2619             }

2620

2621             // Handle triggering a single element

2622

2623             // don't do events on text and comment nodes

2624             if ( !elem || elem.nodeType == 3 || elem.nodeType == 8 )

2625                 return undefined;

2626             

2627             // Clean up in case it is reused

2628             event.result = undefined;

2629             event.target = elem;

2630             

2631             // Clone the incoming data, if any

2632             data = jQuery.makeArray(data);

2633             data.unshift( event );

2634         }

2635

2636         event.currentTarget = elem;

2637

2638         // Trigger the event, it is assumed that "handle" is a function

2639         var handle = jQuery.data(elem, "handle");

2640         if ( handle )

2641             handle.apply( elem, data );

2642

2643         // Handle triggering native .onfoo handlers (and on links since we don't call .click() for links)

2644         if ( (!elem[type] || (jQuery.nodeName(elem, 'a') && type == "click")) && elem["on"+type] && elem["on"+type].apply( elem, data ) === false )

2645             event.result = false;

2646

2647         // Trigger the native events (except for clicks on links)

2648         if ( !bubbling && elem[type] && !event.isDefaultPrevented() && !(jQuery.nodeName(elem, 'a') && type == "click") ) {

2649             this.triggered = true;

2650             try {

2651                 elem[ type ]();

2652             // prevent IE from throwing an error for some hidden elements

2653             } catch (e) {}

2654         }

2655

2656         this.triggered = false;

2657

2658         if ( !event.isPropagationStopped() ) {

2659             var parent = elem.parentNode || elem.ownerDocument;

2660             if ( parent )

2661                 jQuery.event.trigger(event, data, parent, true);

2662         }

2663     },


trigger函数接受四个参数,前三个容易理解,最后一个参数bubbling表示事件是否在冒泡,它是个内部参数。当从外部调用trigger函数时,bubbling参数总是false,即只传递三个参数。trigger函数会递归调用,外部调用trigger(event, data, elem, false),它又会对elem的父结点调用trigger,但这时bubbling参数传递为true(第2659-2661行),即trigger(event, data, elem.parentNode, true)。接着让我们来分析源代码,当从外部调用而非递归调用trigger(bubbling为false)时第2596到2607行构造事件对象,第2610到2619行处理全局事件。2639到2641行触发事件,第2648-2654会模拟浏览器的默认行为,例如当对类型为text的input输入框触发click事件时,会调用该元素的click()方法,其效果会使光标聚集至该输入框。第2658-2662行模拟冒泡行为,即元素的父结点递归调用trigger函数。

关于jQuery的事件处理机制就到这里了,原本要打算讲下live handler的实现机制,限于篇幅,就不讲了,幸好这里也有一篇文章讲到了,有图,有视频,有真相。关于为什么需要mouseenter, mouseleave事件而不是直接利用浏览器内置的mouseover, mouseout事件,则可参见《JQuery in Action》第4.2.6节。

0 评论: