jQuery 1.9.1源码分析系列(十)事件系统之主动触发事件和模拟冒泡处理

发现一个小点,先前没有注意的

   stopPropagation: function() {
      var e = this.originalEvent;
      ...
      if ( e.stopPropagation ) {
        e.stopPropagation();
      }

  jQuery重载stopPropagation函数调用的本地事件对象的stopPropagation函数阻止冒泡。也就是说,阻止冒泡的是当前节点,而不是事件源。

  说到触发事件,我们第一反应是使用$(...).click()这种方式触发click事件。这种方式毫无疑问简洁明了,如果能使用这种方式推荐使用这种方式。但是如果是自定义事件呢?比如定义一个$(document).on("chuaClick","#middle",fn);这种情况怎么触发事件?这就要用到$("#middle").trigger("chuaClick")了。

a.触发事件低级API——jQuery.event.trigger

  trigger函数对所有类型事件的触发提供了支持。这些事件主要分为两类:普通浏览器事件(包含带有命名空间的事件如"click.chua")、自定义事件。因为要统一处理,所以函数内部实现没有调用.click()这种方式来对普通浏览器事件做捷径处理,而是统一流程。处理过程如下

  1.获取要触发的事件(传入的event可能是事件类型而不是事件对象)

event = event[ jQuery.expando ] ? event :new jQuery.Event( type, typeof event === "object" && event );

  2.修正浏览器事件(主要有修正事件源)和组合正确的事件处理参数data

  if ( type.indexOf(".") >= 0 ) {
        //有命名空间的事件触发; 先取出事件处理入口函数handle()使用的事件类型type
        namespaces = type.split(".");
        type = namespaces.shift();
        namespaces.sort();
      }
      ...// 调用者可以传递jQuery.Event对象,普通对象,甚至是字符串
      event = event[ jQuery.expando ] ?
      event :
      new jQuery.Event( type, typeof event === "object" && event );
      event.isTrigger = true;
      event.namespace = namespaces.join(".");
      event.namespace_re = event.namespace ?
      new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) :
      null;
      // 重置result属性,避免上次的结果残留
      event.result = undefined;
      if ( !event.target ) {
        event.target = elem;
      }
      // 克隆传参data并将event放在传参data的前面,创建出事件处理入口函数的参数列表,创建后结果可能是[event,data]
      data = data == null ?
      [ event ] :
      jQuery.makeArray( data, [ event ] );

  后面这段组合事件处理参数列表data在后面处理时调用

  if ( handle ) {
          handle.apply( cur, data );
        }

  3.判断是否是特殊节点对象的的特殊事件,是的话特殊处理

 special = jQuery.event.special[ type ] || {};
  if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {
    return;
  }

  这里面需要特殊处理的事件比较少,这里列一下

 special: {
      click.trigger: function(){   // checkbox, 触发本地事件确保状态正确if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) {
            this.click();
            return false;
          }
      },
      focus.trigger: function() {  // 触发本地事件保证失焦/聚焦序列正确if ( this !== document.activeElement && this.focus ) {
            try {
              this.focus();
              return false;
            } catch ( e ) {
              // Support: IE<9
              // If we error on focus to hidden element (#1486, #12518),
              // let .trigger() run the handlers
            }
          }
      },
      blur.trigger: function() {if ( this === document.activeElement && this.blur ) {
            this.blur();
            return false;
          }
      }
    }

  4.从事件源开始遍历父节点直到Window对象,将经过的节点保存(保存到eventPath)下来备用

for ( ; cur; cur = cur.parentNode ) {
  eventPath.push( cur );
  tmp = cur;
}
// 将window也压入eventPath(e.g., 不是普通对象也不是断开连接的DOM)
if ( tmp === (elem.ownerDocument || document) ) {
  eventPath.push( tmp.defaultView || tmp.parentWindow || window );
}

  5.循环先前保存的节点,访问节点缓存,如果有对应的事件类型处理队列则取出其绑定的事件(入口函数)进行调用。      

// jQuery绑定函数处理:判断节点缓存中是否保存相应的事件处理函数,如果有则执行
       handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" );
       if ( handle ) {
           handle.apply( cur, data );
       }
       // 本地绑定处理
       handle = ontype && cur[ ontype ];
       if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) {
           event.preventDefault();
       }

  6. 最后处理浏览器默认事件,比如submit标签的提交表单处理。

  // 如果没有人阻止默认的处理,执行之
      if ( !onlyHandlers && !event.isDefaultPrevented() ) {
        ...
      }

  注意:普通事件加上命名空间仍然属于普通事件,普通调用方式依然其作用。比如$(document).on('click.chua',"#id",fn1).on("click","#id",fn2);当点击“#id”节点的时候fn1依然会被调用。触发指定命名空间事件的唯一方式是trigger:$("#id").trigger("click.chua"),此时只会调用fn1。

  从第4、5个步骤可以看到trigger的另外一个巨大作用——模拟冒泡处理。后面会分析到

b. 事件特殊处理jQuery.event.special(主要有事件替代、模拟冒泡)详解

  委托设计是基于事件可冒泡的。但是有些事件是不可冒泡的,有的事件在不同的浏览器上支持的冒泡情况不同。还有不同的浏览器支持的事件类型也不尽相同。这些处理主要都被放在jQuery.event.special中。jQuery.event.special对象中保存着为适配特定事件所需的变量和方法。

  具体有:

delegateType / bindType (用于事件类型的调整)
setup (在某一种事件第一次绑定时调用)
add (在事件绑定时调用)
remove (在解除事件绑定时调用)
teardown (在所有事件绑定都被解除时调用)
trigger (在内部trigger事件的时候调用)
noBubble
_default
handle (在实际触发事件时调用)
preDispatch (在实际触发事件前调用)
postDispatch (在实际触发事件后调用)

  看一下模拟冒泡的函数simulate

simulate: function( type, elem, event, bubble ) {
      // 构建一个新的事件以区别先前绑定的事件.
      // 新构建的事件避免阻止冒泡, 但如果模拟事件可以阻止默认操作的话,我们做同样的阻止默认操作。
      var e = jQuery.extend(
        new jQuery.Event(),
        event,
        { type: type,
          isSimulated: true,
          originalEvent: {}
        }
        );
      if ( bubble ) {
        jQuery.event.trigger( e, null, elem );
      } else {
        jQuery.event.dispatch.call( elem, e );
      }
      if ( e.isDefaultPrevented() ) {
        event.preventDefault();
      }
    }

  看到没有,真正模拟冒泡函数是jQuery.event.trigger函数

special第一组

  这里面涉及到冒泡处理的问题。

special: {
  load: {
    //阻止触发image.load事件冒泡到window.load
    noBubble: true
  },
  click: {
    //checkbox触发时保证状态正确
    trigger: function() {if (...) {this.click();return false;}}
  },
  focus: {
    //触发本当前节点blur/focus事件 确保队列正确
    trigger: function() {
      if ( this !== document.activeElement && this.focus ) {
        try {
          this.focus();
          return false;
        } catch ( e ) {
          // IE<9,如果我们错误的让隐藏的节点获取焦点(#1486, #12518),
          // 让.trigger()运行处理器
        }
      }
    },
    delegateType: "focusin"
  },
  blur: {
    trigger: function() {
      if ( this === document.activeElement && this.blur ) {
        this.blur();
        return false;
      }
    },
    delegateType: "focusout"
  },
  beforeunload: {
    postDispatch: function( event ) {
      //即使的returnValue等于undefined,Firefox仍然会显示警告
      if ( event.result !== undefined ) {
        event.originalEvent.returnValue = event.result;
      }
    }
  }
}

  focus/blur本来是不冒泡的,但是我们依然可以通过$(document).on('focus ','#left',fn)来绑定,是怎么做到的?我们来看jQuery的处理

  第一步,将focus绑定的事件转化为focusin来绑定,focusin在W3C的标准中是冒泡的,除开火狐之外的浏览器也确实支持冒泡(火狐浏览器focusin/focusout支持冒泡的兼容后面会详解)

type = ( selector ? special.delegateType : special.bindType ) || type;

  然后,根据新得到的type类型(focusin)获取新的special

special = jQuery.event.special[ type ] || {};  

获取的special结果为

jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
  var attaches = 0,
  handler = function( event ) {
    //模拟冒泡
    jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );
  };
  jQuery.event.special[ fix ] = {
        setup: function() {
          if ( attaches++ === 0 ) {
            document.addEventListener( orig, handler, true );
          }
        },
        teardown: function() {
          if ( --attaches === 0 ) {
            document.removeEventListener( orig, handler, true );
          }
        }
  };
});

  再然后,就是绑定事件,绑定事件实际上就对focusin、focusout做了兼容处理,源码中第一个判断有special.setup.call(…)这段代码,根据上面setup函数可见第一次进入的时候实际上是通过setup函数中的document.addEventListener( orig, handler, true )绑定事件,注意:第一个参数是是orig,因为火狐不支持focusin/focusout所以jQuery使用focus/blur替代来监听事件;注意第三个参数是true,表示在事件捕获阶段触发事件。

  我们知道任何浏览器捕获都是从外层到精确的节点的,所有的focusin事件都会被捕获到,然后执行handler函数(里面是jQuery.event.simulate函数,源码略)。其他事件绑定则进入if分支将事件直接绑定到elem上

if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
  if ( elem.addEventListener ) {
    elem.addEventListener( type, eventHandle, false );
  } else if ( elem.attachEvent ) {
    elem.attachEvent( "on" + type, eventHandle );
  }
}

special第二组:mouseenter/mouseleave

//使用mouseover/out和事件时机检测创建mouseenter/leave事件
jQuery.each({
  mouseenter: "mouseover",
  mouseleave: "mouseout"
  }, function( orig, fix ) {
    jQuery.event.special[ orig ] = {
      delegateType: fix,
      bindType: fix,
      handle: function( event ) {
        var ret,
        target = this,
        related = event.relatedTarget,
        handleObj = event.handleObj;
        //对于mousenter/leave,当related在target外面的时候才调用handler
        //参考: 当鼠标离开/进入浏览器窗口的时候是没有relatedTarget的
        if ( !related || (related !== target && !jQuery.contains( target, related )) ) {
          event.type = handleObj.origType;
          ret = handleObj.handler.apply( this, arguments );
          event.type = fix;
        }
        return ret;
      }
    };
});

  需要注意的是只有在鼠标指针穿过被选元素时,才会触发 mouseenter 事件。对应mouseleave这样的话,mouseenter子元素不会反复触发事件,否则在IE中经常有闪烁情况发生

  使用mouseover/out和事件时机检测创建mouseenter/leave事件有个关键的判断

if ( !related || (related !== target && !jQuery.contains( target, related )) )

  其中!jQuery.contains( target, related )表示related在target外面。我们使用图例来解释

  我们假设处理的是mouseenter事件,进入target。

  鼠标从related到target,很明显related在target外面,所以当鼠标移动到target的时候满足条件,调用处理。

  

  现在反过来,很明显related在target里面,那么鼠标之前就处于mouseenter状态(意味着之前就进行了mouseenter处理器处理),避免重复调用当然是不进行任何处理直接返回了。

  

  我们假设处理的是mouseleave事件,离开target。

  鼠标从target到related,很明显related在target里面,所以当鼠标移动到related的时候依然么有离开target,不做处理。

  

  鼠标从target到related,很明显related在target外面,所以当鼠标移动到related的时候已经离开了target的范围,做处理。

  

special第三组:submit和change

主要是ie下submit不能冒泡的处理

  jQuery.event.special.submit主要有一下几个特征

  setup
  postDispatch
  teardown

  根据添加事件的代码可知添加事件的时候如果符合条件则会调用setup来添加事件

if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false )

  jQuery在ie下模拟submit事件以click和keypress替代,只不过是添加了命名空间来区别和普通click和keypress事件。

setup: function() {
  ...
  jQuery.event.add( this, "click._submit keypress._submit", function( e ) {
    var elem = e.target,
    form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined;
      if ( form && !jQuery._data( form, "submitBubbles" ) ) {
        jQuery.event.add( form, "submit._submit", function( event ) {
          event._submit_bubble = true;
        });
        jQuery._data( form, "submitBubbles", true );
      }
  });
},

  在事件调用过程中(dispatch)会调用postDispatch来处理

if ( special.postDispatch ) {
    special.postDispatch.call( this, event );
}
  postDispatch中调用simulate完成事件处理
postDispatch: function( event ) {
  // If form was submitted by the user, bubble the event up the tree
  if ( event._submit_bubble ) {
    delete event._submit_bubble;
    if ( this.parentNode && !event.isTrigger ) {
      jQuery.event.simulate( "submit", this.parentNode, event, true );
    }
  }
},

  teardown用在删除事件绑定中

  ie下change事件的处理和submit类似,事件使用beforeactivate替代来监听,处理函数变成了handle,在事件分发(dispatch)中执行代码

ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
    .apply( matched.elem, args ); 

主要源码如下

 jQuery.event.special.change = {
      setup: function() {
        //rformElems = /^(?:input|select|textarea)$/i
        if ( rformElems.test( this.nodeName ) ) {
          // IE不会在check/radio失焦前触发change事件; 在属性更改后触发它的click事件
          // 在special.change.handle中会吞掉失焦触发的change事件.
          // 这里任然会在check/radio失焦后触发onchange事件.
          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 && !event.isTrigger ) {
                this._just_changed = false;
              }
              // Allow triggered, simulated change events (#11500)
              jQuery.event.simulate( "change", this, event, true );
            });
          }
          return false;
        }
        // 事件代理; 懒惰模式为后代input节点添加change事件处理
        jQuery.event.add( this, "beforeactivate._change", function( e ) {
          var elem = e.target;
          if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) {
            jQuery.event.add( elem, "change._change", function( event ) {
              if ( this.parentNode && !event.isSimulated && !event.isTrigger ) {
                jQuery.event.simulate( "change", this.parentNode, event, true );
              }
            });
            jQuery._data( elem, "changeBubbles", true );
          }
        });
      },
      handle: function( event ) {
        var elem = event.target;
        // 吞掉本地单选框和复选框的change事件,我们在上面已经出发了事件
        if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) {
          return event.handleObj.handler.apply( this, arguments );
        }
      },
    }

  OK,到此,事件系统也告一个段落了,谢谢大家多多支持。

(0)

相关推荐

  • jQuery中阻止冒泡事件的方法介绍

    一.冒泡事件简介 当我们点击一个控件的时候,如果包括这个控件的父控件也有click事件,则会继续执行.比如:div下的a都有click事件,点击a的时候,会alert出现2次.这个现象叫做冒泡事件. 这个事件从原始元素开始一直冒泡到DOM树的最上层.目标元素: 任何一个事件的目标元素都是最开始的那个元素,在我们的这个例子中也就是按钮,并且它在我们的元素对象中以属性的形式出现.使用事件代理的话我们可以把事 件处理器添加到一个元素上,等待一个事件从它的子级元素里冒泡上来,并且可以很方便地得知这个事件

  • JQuery中DOM事件冒泡实例分析

    本文实例分析了JQuery中DOM事件冒泡.分享给大家供大家参考.具体分析如下: 什么是冒泡 在页面上可以有多个事件,也可以多个元素响应同一个事件.假设网页上有两个元素,其中一个元素嵌套在另一个元素里,并且都被绑定了click事件,同时body元素上也绑定了click事件. <div id="content"> 外层div元素 <span>内层span元素</span> 外层div元素 </div> <script type=&qu

  • jQuery中事件对象e的事件冒泡用法示例介绍

    之前查手册的时候没有看到有事件对象这一概念,当时我想实现的是点击一个文本框出现一个下拉多选框,在文本框失去焦点是触发blur事件,从而使下拉框隐藏起来.但是当我要选择多选框是也会使它隐藏,就不能进行选择了,这让我很郁闷.查了一天的资料,终于在脱离了焦点这一块.在网上发现有一个事件冒泡的东西,发现通过点击可以实现我的这一功能. e.stopPropagation()阻止事件冒泡 复制代码 代码如下: <head> <title></title> <script sr

  • 利用JQuery阻止事件冒泡

    冒泡事件就是点击子节点,会向上触发父节点,祖先节点的点击事件. 我们在平时的开发过程中,肯定会遇到在一个div(这个div可以是元素)包裹一个div的情况,但是呢,在这两个div上都添加了事件,如果点击里面的div我们希望处理这个div的事件,但是呢,我们不希望外层的div的事件也执行,这时候我们就要用到阻止冒泡. 通俗点来说吧,你在家里看电视,躲在自己的小房间,但是你不希望声音传到隔壁父母的耳朵里,这时候,你可能躲在被窝里,或者墙壁的隔音效果很好,阻隔声音可以理解为阻止冒泡. <style>

  • 理解jquery事件冒泡

    一.什么是jquery事件冒泡 在很多教材或者手册都可能会涉及到事件冒泡的概念,老手来说这当然是最基本的概念,但往往对于初学者可能比较陌生或者说从来没有听说过.下面就结合代码实例来简单介绍一下什么是事件冒泡. 代码实例如下: <html> <head> <meta charset=" gb2312"> <title>事件冒泡</title> <script type="text/javascript"

  • jQuery中on绑定事件后引发的事件冒泡问题如何解决

    用on绑定时,我把子元素的 绑定到 document,而把父元素绑定到上级元素,导致 return false 阻止冒泡无效. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999

  • jquery取消事件冒泡的三种方法(推荐)

    1.通过返回false来取消默认的行为并阻止事件起泡. jQuery 代码: $("form").bind( "submit", function() { return false; } ); 2.通过使用 preventDefault() 方法只取消默认的行为. jQuery 代码: $("form").bind( "submit", function(event){ event.preventDefault(); } );

  • 深入理解jQuery之防止冒泡事件

    冒泡事件就是点击子节点,会向上触发父节点,祖先节点的点击事件. 下面是html代码部分: <body> <div id="content"> 外层div元素 <span>内层span元素</span> 外层div元素 </div> <div id="msg"></div> </body> 对应的jQuery代码如下: <script type="text/

  • js阻止冒泡及jquery阻止事件冒泡示例介绍

    js阻止冒泡 在阻止冒泡的过程中,W3C和IE采用的不同的方法,那么我们必须做以下兼容. 复制代码 代码如下: function stopPro(evt){ var e = evt || window.event; //returnValue如果设置了该属性,它的值比事件句柄的返回值优先级高.把这个属性设置为 fasle, //可以取消发生事件的源元素的默认动作. //window.event?e.returnValue = false:e.preventDefault(); window.ev

  • JavaScript和JQuery的鼠标mouse事件冒泡处理

    简单的鼠标移动事件: 进入 复制代码 代码如下: mouseenter:不冒泡 mouseover: 冒泡 不论鼠标指针穿过被选元素或其子元素,都会触发 mouseover 事件 只有在鼠标指针穿过被选元素时,才会触发 mouseenter 事件 移出 复制代码 代码如下: mouseleave: 不冒泡 mouseout:冒泡 不论鼠标指针离开被选元素还是任何子元素,都会触发 mouseout 事件 只有在鼠标指针离开被选元素时,才会触发 mouseleave 事件 我们通过一个案例观察下问题

  • jquery关于事件冒泡和事件委托的技巧及阻止与允许事件冒泡的三种实现方法

    首先,大家都知道,jQuery事件触发时有2种机制,一种是事件委托,另一种是事件冒泡(IE情况暂时不考虑).拿click事件做例子,先附上一段代码: html: <body> <div id="box"> <p id="btn">我是按钮</p> </div> </body> style: .hid{ display:none; } script: $('#box').click(functio

  • 深入理解jQuery中的事件冒泡

    1.什么是冒泡 eg: <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>事件冒泡</title> <script src="

随机推荐