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 if( event.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: function( event, 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节。