jQuery代码分析之三 Event

来源:互联网 发布:python exit code非0 编辑:程序博客网 时间:2024/05/22 13:06

jQuery代码分析之三 Event

本文的代码分析基于jQuery 1.7

jQuery事件注册的核心是jQuery.event对象,据类库中的注释介绍,其大部分思想是从Dean Edwards那儿借来的,罗嗦两句,Dean Edwards是个JavaScript大神,很多流行的类库的基本思想从他那儿借来的。例如,让JavaScript能够用更加面向对象的方式组织起来,关于这方面的实践,他算贡献很大的。当然,他本人最有名的类库就是base2,它可以方便我们实现继承,命名空间,模块化等。

下面给出jQuery.event对象的声明

jQuery.event = {add: function( elem, types, handler, data, selector ) {},global: {},remove: function( elem, types, handler, selector ) {},customEvent: {"getData": true,"setData": true,"changeData": true},trigger: function( event, data, elem, onlyHandlers ) {},dispatch: function( event ) {},props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks: {},keyHooks: {},mouseHooks: {},fix: function( event ) {},special: {ready: {},focus: {},blur: {},beforeunload: {}},simulate: function( type, elem, event, bubble ) {}

看起来整个接口定义非常清楚,添加/删除事件监听函数,事件触发函数,事件分派(dispatch)函数。为了弄清楚这些函数的实现机理,我们决定从功能出发来说明为什么要这样实现。

怎样实现注册事件时带命名空间?

从add函数入手,上add函数的代码,由于篇幅有限,我删除了一些局部变量申明和只为了增强程序健壮性的语句:

function( elem, types, handler, data, selector ) {// Make sure that the handler has a unique ID, used to find/remove it later// 这个地方会给每个handler一个唯一的ID,我们知道Javascript是单线程执行的,所以这个地方没有所谓的并发问题// 既然每个handler对象的guid值都不一样,那对同一个事件的回调函数就不会相互覆盖了。if ( !handler.guid ) {handler.guid = jQuery.guid++;}// Init the element's event structure and main handler, if this is the firstevents = elemData.events;if ( !events ) {elemData.events = events = {};}eventHandle = elemData.handle;// 这儿初始化元素的时间回调函数,那为什么要给它定义一个handle的函数呢?// 原来addEventListener和attachEvent接受一个回调函数或者是一个实现了interface EventListener的对象// 我们这儿就是选择后者。if ( !eventHandle ) {elemData.handle = eventHandle = function( e ) {// Discard the second event of a jQuery.event.trigger() and// when an event is called after a page has unloadedreturn typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ?// 这儿调用了dispatch函数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 eventseventHandle.elem = elem;}// Handle multiple events separated by a space// jQuery(...).bind("mouseover mouseout", fn);// 我们可以用一个回调函数监听多个事件types = hoverHack(types).split( " " );for ( t = 0; t < types.length; t++ ) {tns = rtypenamespace.exec( types[t] ) || [];type = tns[1];namespaces = ( tns[2] || "" ).split( "." ).sort();// If event changes its type, use the special event handlers for the changed typespecial = jQuery.event.special[ type ] || {};// If selector defined, determine special event api type, otherwise given typetype = ( selector ? special.delegateType : special.bindType ) || type;// Update special based on newly reset type// 在下面我们会介绍special是为何special = jQuery.event.special[ type ] || {};// handleObj is passed to all event handlershandleObj = jQuery.extend({type: type,origType: tns[1],data: data,handler: handler,guid: handler.guid,selector: selector,namespace: namespaces.join(".") // 这儿记录了事件名的命名空间,在分派回调函数时会利用它比较}, handleObjIn );// Delegated event; pre-analyze selector so it's processed quickly on event dispatch// 如果选择子是id/class/tag,那么我们就可以更快尽快事件分派if ( selector ) {handleObj.quick = quickParse( selector );if ( !handleObj.quick && jQuery.expr.match.POS.test( selector ) ) {handleObj.isPositional = true;}}// Init the event handler queue if we're the firsthandlers = events[ type ];if ( !handlers ) {handlers = events[ type ] = [];handlers.delegateCount = 0;// Only use addEventListener/attachEvent if the special events handler returns falseif ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {// Bind the global event handler to the element// 再次提醒:eventHandle不是一个函数对象,而是一个实现了EventListener接口的对象if ( elem.addEventListener ) {elem.addEventListener( type, eventHandle, false );} else if ( elem.attachEvent ) {elem.attachEvent( "on" + type, eventHandle );}}}if ( special.add ) {special.add.call( elem, handleObj );if ( !handleObj.handler.guid ) {handleObj.handler.guid = handler.guid;}}// Add to the element's handler list, delegates in front// 关于这部分的详细描述见下图,handlers的前delegateCount个元素是代理事件回调函数,// 后面的才是直接事件回调函数if ( selector ) {handlers.splice( handlers.delegateCount++, 0, handleObj );} else {handlers.push( handleObj );}// Keep track of which events have ever been used, for event optimizationjQuery.event.global[ type ] = true;}// Nullify elem to prevent memory leaks in IEelem = null;}

另外我还画了一张图来描述整个过程中操作到的数据结构:

Direct and delegated events

在大多数情况下事件都是从源,也就是文档模型中触发该事件最里面的那个元素出发,然后一直向上传播直到document。但是在IE8-中,有些事件例如change和submit并不会自动向上传播,为了保持浏览器间的兼容性,jQuery自定义了这些事件的处理让它们看起来像是可以bubble上去的。

  • Direct event handler每次只要被触发都会调用,不管它是自身触发,还是从子元素传播过来的
  • Delegated event handler只有当事件在bubble到绑定元素的过程中,注意是到绑定元素的过程中,而不是一直到document,对匹配选择子的子元素才管用,这些满足条件的子元素在事件绑定时不必是已经存在在DOM结构中,所以这个对于动态生成的DOM元素绑定事件就非常有效。但是事件绑定时赋予的那个元素当时必须存在在DOM结构中。还有一种情况delegated event handler也特别有用,比如我们需要对1000个元素注册事件,$("#dataTable tbody tr").on("click", function(event){alert($(this).text());});如果用delegated event handler的话,可以简单写作$("#dataTable tbody").on("click", "tr", function(event){alert($(this).text());});
// IE change delegation and checkbox/radio fixif ( !jQuery.support.changeBubbles ) {jQuery.event.special.change = {setup: function() {if ( rformElems.test( this.nodeName ) ) {// IE doesn't fire change on a check/radio until blur; trigger it on click// after a propertychange. Eat the blur-change in special.change.handle.// This still fires onchange a second time for check/radio after blur.if ( this.type === "checkbox" || this.type === "radio" ) {jQuery.event.add( this, "propertychange._change", function( event ) {if ( event.originalEvent.propertyName === "checked" ) {this._just_changed = true;}});jQuery.event.add( this, "click._change", function( event ) {if ( this._just_changed ) {this._just_changed = false;jQuery.event.simulate( "change", this, event, true );}});}return false;}// Delegated event; lazy-add a change handler on descendant inputsjQuery.event.add( this, "beforeactivate._change", function( e ) {var elem = e.target;if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) {jQuery.event.add( elem, "change._change", function( event ) {if ( this.parentNode && !event.isSimulated ) {jQuery.event.simulate( "change", this.parentNode, event, true );}});elem._change_attached = true;}});},handle: function( event ) {var elem = event.target;// Swallow native change events from checkbox/radio, we already triggered them aboveif ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) {return event.handleObj.handler.apply( this, arguments );}},teardown: function() {jQuery.event.remove( this, "._change" );return rformElems.test( this.nodeName );}};}

Fix和Hooks

我们注意到jQuery.event有个成员函数叫fix。这是个什么东东?让我们回顾下jQuery.event的定义,发现fixHooks只是个空对象,搜索下,发现在下面这个地方往里面添加了一些值

// 这段代码主要是实现$("#id").click()或者$("#id").click(function(e) {})这个功能滴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 bindingjQuery.fn[ name ] = function( data, fn ) {if ( fn == null ) {fn = data;data = null;}// 如果没参数就是触发事件,否则就是绑定事件// 注意这个地方this指向jQuery.fn,因为这个函数成为了jQuery.fn的成员函数return arguments.length > 0 ?this.bind( name, data, fn ) :this.trigger( name );};if ( jQuery.attrFn ) {jQuery.attrFn[ name ] = true;}// 这个地方识别出key event,然后指明这个key event处理时需要附加上// keyHooks里的属性集合: char charCode key keyCodeif ( rkeyEvent.test( name ) ) {jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks;}// 这个地方识别出mouse event,然后指明这个mouse event处理时需要附加上// mouseHooks里的属性集合: button buttons clientX clientY fromElement// offsetX offsetY pageX pageY screenX screenY toElement wheelDeltaif ( rmouseEvent.test( name ) ) {jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks;}});

至此整个fix函数的实现就只是属性值拷贝了

function( event ) {// Create a writable copy of the event object and normalize some propertiesvar i, prop,originalEvent = event,fixHook = jQuery.event.fixHooks[ event.type ] || {},copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;event = jQuery.Event( originalEvent );for ( i = copy.length; i; ) {prop = copy[ --i ];event[ prop ] = originalEvent[ prop ];}// 省略了一些对特别浏览器的bug处理代码return fixHook.filter? fixHook.filter( event, originalEvent ) : event;}

怎样兼容不同的事件触发模式?

首先介绍下W3C定义的事件触发模型

event对象必须经历三个阶段:capture phase; target phase; and bubble phase。如下图所示。这些阶段并不是都必须的,但是任一个阶段要么不被浏览器支持,要么被程序跳过。例如我们调用Event.bubbles=false,那么bubble阶段就会被跳过。如果在事件被分发之前调用Event.stopPropagation(),那么所有的阶段都不会被执行。

  • capture phase

    the event object must propagate through the target's ancestors from the defaultView to the target's parent. This phase is also known as the capturing phase. Event listeners registered for this phase must handle the event before it reaches its target.

  • target phase

    the event object must arrive at the event object's event target. This phase is also known as the at-target phase. Event listeners registered for this phase must handle the event once it has reached its target. If the event type indicates that the event must not bubble, the event object must halt after completion of this phase.

  • bubble phase

    the event object propagates through the target's ancestors in reverse order, starting with the target's parent and ending with the defaultView. This phase is also known as the bubbling phase. Event listeners registered for this phase must handle the event after it has reached its target.

下面是这一模型的一个示意图


为了理解这一过程,我们可以在Firefox下运行如下代码:(偶用的是FF16)

function onload(event) {document.getElementById("span1").addEventListener("click", function() {console.log("from span1");});document.getElementById("divChild").addEventListener("click", function() {console.log("from child bubble");});document.getElementById("divChild").addEventListener("click", function() {console.log("from child capture");}, true);document.getElementById("divParent").addEventListener("click", function() {console.log("from parent bubble");});document.getElementById("divParent").addEventListener("click", function() {console.log("from parent capture");}, true);}
#span1 {border:10px solid #80e0e0;text-align:center;}#span1:hover {cursor:pointer;}#divChild {border:10px solid red;width:200px;}#divParent {border:10px solid blue;width:220px;}
<body onload="onload()"><div id="divParent"><div id="divChild"><div id="span1">click me</div></div></div></body>

运行结果如下:

  • from parent capture
  • from child capture
  • from span1
  • from child bubble
  • from parent bubble

我们再来看看IE下事件的生命周期

An event has a life cycle that begins with the action or condition that initiates the event and ends with the final response by the event handler or Internet Explorer. The life cycle of a typical event consists of the following steps.

  • The user action or condition associated with the event occurs.
  • The event object is instantly updated to reflect the conditions of the event.
  • The event fires. This is the actual notification in response to the event.
  • The event handler associated with the source element is called, carries out its actions, and returns.
  • The event bubbles up to the next element in the hierarchy, and the event handler for that element is called. This step repeats until the event bubbles up to the window object or a handler cancels bubbling.
  • The final default action, if any, is taken, but only if this action has not been canceled by a handler.

The IE event model does not have any notion of event capturing, as the DOM Level 2 model does. However, events do bubble up through the containment hierarchy in the IE model, just as they do in the Level 2 model. As with the Level 2 model, event bubbling applies only to raw or input events (primarily mouse and keyboard events), not to higher-level semantic events.

The primary difference between event bubbling in the IE and DOM Level 2 event models is the way that you stop bubbling. The IE Event object does not have a stopPropagation( ) method, as the DOM Event object does. To prevent an event from bubbling or stop it from bubbling any further up the containment hierarchy, an IE event handler must set the cancelBubble property of the Event object to true:window.event.cancelBubble = true; Note that setting cancelBubble applies only to the current event. When a new event is generated, a new Event object is assigned to window.event, and cancelBubble is restored to its default value of false.

注意内存泄漏

var i;var els = document.getElementsByTagName('*');// Case 1for(i=0 ; i<els.length ; i++){  els[i].addEventListener("click", function(e){/*do something*/}, false});}// Case 2function processEvent(e){  /*do something*/}for(i=0 ; i<els.length ; i++){  els[i].addEventListener("click", processEvent, false});}

In the first case, a new (anonymous) function is created at each loop turn. In the second case, the same previously declared function is used as an event handler. This results in smaller memory consumption. Moreover, in the first case, since no reference to the anonymous functions is kept, it is not possible to call element.removeEventListener because we do not have a reference to the handler, while in the second case, it's possible to do myElement.removeEventListener("click", processEvent, false).

参考文章

  • http://msdn.microsoft.com/en-us/library/ms533023%28v=vs.85%29.aspx#The_Life_Cycle_of_an
  • http://www.w3.org/TR/DOM-Level-3-Events/#dom-event-architecture
  • https://developer.mozilla.org/en-US/docs/DOM/element.addEventListener
  • http://docstore.mik.ua/orelly/webprog/jscript/ch19_03.htm
原创粉丝点击