jQuery的事件绑定有几个比较优秀的特点:

  1. 可以绑定不限数量的处理函数

  2. 事件可以委托到祖先节点,不必一定要绑到对应的节点,这样后添加的节点也照样能被处理。

  3. 链式操作

  

  下面主要分析事件的委托设计。事件源我们成为委托节点,委托节点委托他的祖先节点替他执行事件处理,这个祖先节点被成为被委托节点。

  DOM的原生事件将处理绑定在相应的节点上,相应节点触发事件才能执行处理。将事件处理委托给祖先节点,这个事件处理是附加到祖先节点的。那么需要做到的是,原节点触发了事件,想要执行已经附加到祖先节点的事件处理那么就需要保证祖先节点也需要触发相同的事件,并且知道实际上是触发了原节点。如何能做到这一点?事件冒泡机制提供了这种可能。

a. 深入理解事件冒泡


  先放一张自己画的事件模型

  

  DOM的事件流程分为三个阶段:

  第一阶段:事件捕获。所谓的事件捕获就是从最大范围(document)开始,一级一级往下找,知道找到最精确的事件源(触发事件的节点,模型中事件源是#small节点)为止的过程。举个简单的例子,要找到A学校B年级C班D同学;那我先要找到A学校,然后找到B年级,然后在找到C班,最后我才能准确的找到D同学。

  第二阶段:事件触发。找到了事件源,接下来就应当执行绑定的相应的事件(如果有的话)。举个例:告诉D同学到12点了(这个就好比是事件类型),然后D同学就知道到是吃中午饭的时间了,然后就去吃中午饭(执行相应的事件)。

  第三阶段:事件冒泡。事件源执行完事件处理后。这个类型的事件会向祖先节点传递,直到document(包括document)(当然是在事件冒泡没有被阻止的前提下,后续的分析都是基于这个前提下)。也就是说事件源的每一个祖先节点都会触发同类事件。还是上面的例子,D同学去吃饭了,然后C班知道到12点了(事件类型),然后全班放学(执行事件)...最后A学校也收到了12点的消息(事件类型),然后学校下课铃响了(执行相应事件)。当然这只是一个例子,里面的内容不必当真。

  

  事件冒泡这么好的特性jQuery当然要好好利用。事件源触发了某个事件,其祖先节点必然也会触发该类事件,而且祖先还知道冒泡到我这里的事件的事件源是那个后代节点(event.target)。这便给了事件委托提供了必要条件。

  想象一下,把事件源a触发click事件的处理委托给其祖先节点b。当a节点触发click,事件冒泡到b的时候,b节点也触发click事件,然后b一看事件列表中有一个委托事件,这个委托事件保存了委托节点的选择器,这个选择器所匹配节点就是事件源a,那么b马上执行这个委托事件(当然jQuery做的更为复杂一些,委托节点只要是a到b之间的节点且事件类型也和触发的事件类型相同就会执行其委托处理)。

  举个实例

<style>
#big{
width: 400px;
height: 400px;
background-color: #00f;
}
#middle{
width: 200px;
height: 200px;
background-color: #000;
}
#small{
width: 100px;
height: 100px;
background-color: #f00;
}
</style> <div id="big"><div id="middle"><div id="small"></div></div></div>
<script>
document.getElementById('big').onclick = function(){console.log("big clicked!")}
document.getElementById('middle').onclick = function(){console.log("middle clicked!")}
document.getElementById('small').onclick = function(){console.log("small clicked!")}
</script>

  点击最小的那个红块(#small)。执行结果如下

  

b. jQuery事件委托处理流程


  上一章分析jQuery.event.add的时候已经分析了事件绑定,再把绑定的部分源码抽出来

   if ( !(eventHandle = elemData.handle) ) {
eventHandle = elemData.handle = function( e ) {
//当一个事件被调用后页面已经卸载,则放弃jQuery.event.trigger()的第二个事件,
return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ?
jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
undefined;
};
//将elem作为handle函数的一个特征防止ie非本地事件引起的内存泄露
eventHandle.elem = elem;
}
...
//非自定义事件,如果special事件处理器返回false,则只能使用addEventListener/attachEvent
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 );
}
}

  绑定到elem上的事件处理是eventHandle,最终执行eventHandle的时候实际执行的是事件调度jQuery.event.dispatch。事件调度的的流程实际上就是处理委托事件的流程,因为本节点的响应处理最终会被附加到委托处理列表后面。

  事件调度流程为

  1. 从本地事件对象event构造一个可写的jQuery.Event对象。并用这个对象替换掉传参中的本地事件对象

//从本地事件对象构造一个可写的jQuery.Event
event = jQuery.event.fix( event );
...
//使用修正过得jQuery.Event而不是(只读的)本地事件
args[0] = event;
event.delegateTarget = this;

  本地事件event的结构如下

  

  使用本地事件构造的新事件对象jQuery.Event结构如下

  

  其中originalEvent属性的值便是本地事件对象。构造的这个事件对象有很多属性都是直接从本地事件对象中抽出来的。

  2. 获取当前节点缓存中对应事件类型的事件处理列表

        handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [],

  事件处理列表的的顺序是委托事件处理在前面,最后才是直接绑定到当前节点的事件处理。

  3. 用当前节点替换jQuery.event.handlers的调用者并执行之,获取到符合要求的委托处理函数队列(这个队列最后会加上绑定到节点本身的处理事件)

//获取指定的事件处理队列,主要使用event.target事件源节点不断循环往上查找父节点,
//看些节点和是否在handlers中的选择器对应的节点中
handlerQueue = jQuery.event.handlers.call( this, event, handlers );

  详细分析一下jQuery.event.handlers中获取符合要求的委托处理函数队列。

  jQuery.event.handlers先将委托事件处理取出来放在处理队列handlerQueue中。

  查找的过程是:先取出事件源cur = event.target;然后在确定有委托处理的情况下从事件源开始往他的祖先节点查询,遍历委托事件列表中的每一个委托事件处理所指定的响应节点(委托事件处理对象的selector所指定)是否包含查询的节点【handleObj.needsContext ?jQuery( sel, this ).index( cur ) >= 0 :jQuery.find( sel, this, null, [ cur ] ).length】,如果包含则往事件处理队列handlerQueue中压入该委托处理。源码如下

if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) {
  //冒泡父节点,找到匹配的委托事件存入handlerQueue队列
  for ( ; cur != this; cur = cur.parentNode || this ) {
    if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) {
      matches = [];
      for ( i = 0; i < delegateCount; i++ ) {
        // 避免和Object.prototype属性冲突(#13203)
        sel = handleObj.selector + " ";
        if ( matches[ sel ] === undefined ) {
          matches[ sel ] = handleObj.needsContext ?
          jQuery( sel, this ).index( cur ) >= 0 :
          jQuery.find( sel, this, null, [ cur ] ).length;
        }
          
        if ( matches[ sel ] ) {
          matches.push( handleObj );
        }
      }
      //添加委托处理到队列中
      if ( matches.length ) {
        handlerQueue.push({ elem: cur, handlers: matches });
      }
    }
  }
}

  最后将直接绑定到当前节点的处理也压入执行

//添加直接绑定的事件到handlerQueue队列中
if ( delegateCount < handlers.length ) {
  handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });
}

  

  4. 执行事件处理队列handlerQueue中的处理函数

//先运行代理,他们可能是阻止冒泡的,我们可以利用这一点
i = 0;
while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
event.currentTarget = matched.elem; j = 0;
while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { //触发事件的条件:1)没有命名空间,或
// 2)有命名空间的子集或等于那些边界事件(他们两者都可以没有命名空间)
if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { event.handleObj = handleObj;
event.data = handleObj.data;
//执行
ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
.apply( matched.elem, args ); if ( ret !== undefined ) {
if ( (event.result = ret) === false ) {
event.preventDefault();
event.stopPropagation();
}
}
}
}
}

  这里面有一个点需要注意。构造的新事件对象event本来是被委托的节点的事件对象(event.currentTarget可以作证),但是在执行被委托的处理的的时候,事件对象被转换成了委托节点的事件对象(event.currentTarget = matched.elem;),这样保证被委托事件处理中调用的事件对象的正确性。举例:还是前面的那段html,只不过执行代码变成了

function dohander(e){
  alert("dohander");
};
function dot(e){
  e.stopPropagation();
  alert("dot");
};
$(document).on("click",'#big',dohander)
.on("click",'#middle',dot)
.on("click",'#small',dohander);

  "#middle"节点委托document节点执行处理dot,dot中有一段代码e.stopPropagation();当点击"#middle"节点,事件冒泡到document。在执行dot之前如果事件没有被转换成被委托的节点的事件,那么这个阻止冒泡并不是阻止"#middle"节点的事件冒泡,而是阻止document节点的事件冒泡。

  当然,实际上在处理被委托的事件的时候,事件已经冒泡到被委托节点document了。我们是没法对已经冒泡过了的节点进行阻止,比如"#big"节点就已经触发过了click事件。但是我们可以对被委托到document节点的委托事件处理列表做模拟阻止冒泡处理。点击"#middle"节点,事件冒泡到document,执行jQuery.event.handlers得到委托事件处理列表是这样的

//"#small"节点的委托事件处理已经被jQuery.event.handlers过滤掉了
handlerQueue = ["#middle"节点的委托事件处理dot, "#big"节点的委托事件处理dohander];

  执行dot中的e.stopPropagation(),stopPropagation也是重载过的,源码如下

        stopPropagation: function() {
var e = this.originalEvent; this.isPropagationStopped = returnTrue;
if ( !e ) {
return;
}
// If stopPropagation exists, run it on the original event
if ( e.stopPropagation ) {
e.stopPropagation();
} // Support: IE
// Set the cancelBubble property of the original event to true
e.cancelBubble = true;
}

  其中this.isPropagationStopped = returnTrue;事件被标记为阻止冒泡。执行完dot后进入下一个执行handerQueue的下一个委托处理“"#big"节点的委托事件处理dohander”,这里有一个判断

while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) 

  事件event.isPropagationStopped() = returnTrue() = (function returnTrue() {return true;})();阻止进入while执行下一个委托处理。OK,后面的委托事件全部被阻止处理了,所以最终点击“#middle”节点只执行了dot函数。

  

  模拟阻止冒泡过程结束。

  下面用一个更加完整的实例来说明,js代码

  function dohander(e){
alert("dohander");
};
function dot(e){
e.stopPropagation();
alert("dot");
};
function doBigOnly(e){
alert("big");
}
$(document).on("click",'#big',dohander)
.on("click",'#middle',dot)
.on("click",'#small',dohander); $('#big').click(doBigOnly);

  点击“#middle”节点,执行结果:先弹出alert("big"),然后弹出alert("dot")

  

  流程分析:

  点击"#middle","#middle"上没有绑定事件。

  事件冒泡到"#big","#big"绑定了处理doBigOnly直行之显示alert("big")。

  事件冒泡到body,

  然后冒泡到html,

  最后冒泡到document。document上有绑定事件,根据事件源过滤掉"#small"的委托处理。剩下两个委托("#middle"委托处理和"#big"委托处理)需要处理。先处理"#middle"委托处理dot,直行之显示alert("dot"),阻止事件冒泡。后续委托被阻止。

  完毕。

  如果觉得本文不错,请点击右下方【推荐】!

  

  

  

  

jQuery-1.9.1源码分析系列(十) 事件系统——事件委托的更多相关文章

  1. jQuery源码分析系列

    声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://git ...

  2. [转]jQuery源码分析系列

    文章转自:jQuery源码分析系列-Aaron 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://github.com/JsAaro ...

  3. jQuery源码分析系列(转载来源Aaron.)

    声明:非本文原创文章,转载来源原文链接Aaron. 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://github.com/JsAa ...

  4. jQuery源码分析系列——来自Aaron

    jQuery源码分析系列——来自Aaron 转载地址:http://www.cnblogs.com/aaronjs/p/3279314.html 版本截止到2013.8.24 jQuery官方发布最新 ...

  5. jQuery-1.9.1源码分析系列完毕目录整理

    jQuery 1.9.1源码分析已经完毕.目录如下 jQuery-1.9.1源码分析系列(一)整体架构 jQuery-1.9.1源码分析系列(一)整体架构续 jQuery-1.9.1源码分析系列(二) ...

  6. jquery2源码分析系列

    学习jquery的源码对于提高前端的能力很有帮助,下面的系列是我在网上看到的对jquery2的源码的分析.等有时间了好好研究下.我们知道jquery2开始就不支持IE6-8了,从jquery2的源码中 ...

  7. MyCat源码分析系列之——结果合并

    更多MyCat源码分析,请戳MyCat源码分析系列 结果合并 在SQL下发流程和前后端验证流程中介绍过,通过用户验证的后端连接绑定的NIOHandler是MySQLConnectionHandler实 ...

  8. MyCat源码分析系列之——SQL下发

    更多MyCat源码分析,请戳MyCat源码分析系列 SQL下发 SQL下发指的是MyCat将解析并改造完成的SQL语句依次发送至相应的MySQL节点(datanode)的过程,该执行过程由NonBlo ...

  9. MyCat源码分析系列之——BufferPool与缓存机制

    更多MyCat源码分析,请戳MyCat源码分析系列 BufferPool MyCat的缓冲区采用的是java.nio.ByteBuffer,由BufferPool类统一管理,相关的设置在SystemC ...

  10. MyCat源码分析系列之——前后端验证

    更多MyCat源码分析,请戳MyCat源码分析系列 MyCat前端验证 MyCat的前端验证指的是应用连接MyCat时进行的用户验证过程,如使用MySQL客户端时,$ mysql -uroot -pr ...

随机推荐

  1. 执行CSRF令牌所有形式使用POST方法

    从而在并未授权的情况下执行在权限保护之下的操作,有很大的危害性. php CSRF Guardfunction csrfguard_generate_token($unique_form_name){ ...

  2. 在 IIS 7.5 中,应用程序池有两种运行模式:集成模式和经典模式。

    应用程序池模式会影响服务器处理托管代码请求的方式. 如果托管应用程序在采用集成模式的应用程序池中运行,服务器将使用 IIS 和 ASP.NET 的集成请求处理管道来处理请求. 如果托管应用程序在采用经 ...

  3. Sequence Project Showplan Operator 序列映射运算符

    Sequence Project Showplan Operator 序列映射运算符 序列映射运算符会从一个已经排序的集合里通过不停添加集合里的列执行计算. 运算符根据一个或多个列的值把输入集合分为多 ...

  4. ADO.NET Entity Framework CodeFirst 如何输出日志(EF 5.0)

    ADO.NET Entity Framework CodeFirst 如何输出日志(EF4.3) 用的EFProviderWrappers ,这个组件好久没有更新了,对于SQL执行日志的解决方案的需求 ...

  5. UI控件(UIWebView)

    本文主要记录UIWebView三方面内容: 1.基本的加载网页链接或文件: 2.网页js调用原生,也就是Cordova混合架构的原理: 3.原生调用js程序: 原生部分主要代码: @implement ...

  6. 【译】.NET中六个重要的概念:栈、堆、值类型、引用类型、装箱和拆箱

    为何要翻译 一来是为了感受国外优秀技术社区知名博主的高质量文章,二来是为了复习对.NET技术的基础拾遗达到温故知新的效果,最后也是为了锻炼一下自己的英文读写能力.因为是首次翻译英文文章(哎,原谅我这个 ...

  7. SQL Server 中的事务与事务隔离级别以及如何理解脏读, 未提交读,不可重复读和幻读产生的过程和原因

    原本打算写有关 SSIS Package 中的事务控制过程的,但是发现很多基本的概念还是需要有 SQL Server 事务和事务的隔离级别做基础铺垫.所以花了点时间,把 SQL Server 数据库中 ...

  8. Atitit 边缘检测原理attilax总结

    Atitit 边缘检测原理attilax总结 1. 边缘检测的概念1 1.1. 边缘检测的用途1 2. 边缘检测方法分类1 3. 边缘检测的基本方法2 3.1. Roberts边缘检测算子2 3.2. ...

  9. Angular $watch

    如果想在某个属性发生变化的时候执行某些操作,那么scope.$watch是最佳选择 https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$w ...

  10. 修改input框默认黄色背景

    input:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill { -webkit-box-shadow: 0 0 ...