【jQuery源码】事件委托

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"),阻止事件冒泡。后续委托被阻止。

时间: 2024-08-08 19:36:34

【jQuery源码】事件委托的相关文章

jQuery源码解析之on事件绑定

本文采用的jQuery源码为jquery-3.2.1.js jquery的on方法用来在选定的元素上绑定一个或多个事件处理函数. 当参数selector存在时,通常会用来对已经存在的元素或将来即将添加到文档中的元素做事件委托,表示当点击document中的selector元素时,将触发function回调函数. 1 <div id="div" style="font-weight:800;font-size:24px;text-align:center;color:re

[转]jQuery源码分析系列

文章转自:jQuery源码分析系列-Aaron 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://github.com/JsAaron/jQuery 正在编写的书 - jQuery架构设计与实现 本人在慕课网的教程(完结) jQuery源码解析(架构与依赖模块) 64课时 jQuery源码解析(DOM与核心模块)64课时 jQuery源码分析目录(完结) jQuery源码分析系列(01) : 整体架构 jQuery源码分析系列(

jquery源码学习

jQuery 源码学习是对js的能力提升很有帮助的一个方法,废话不说,我们来开始学习啦 我们学习的源码是jquery-2.0.3已经不支持IE6,7,8了,因为可以少学很多hack和兼容的方法. jquery-2.0.3的代码结构如下 首先最外层为一个闭包, 代码执行的最后一句为window.$ = window.jquery = jquery 让闭包中的变量暴露倒全局中. 传参传入window是为了便于压缩 传入undefined是为了undifined被修改,他是window的属性,可以被修

jQuery源码分析系列(33) : AJAX中的前置过滤器和请求分发器

jQuery1.5以后,AJAX模块提供了三个新的方法用于管理.扩展AJAX请求,分别是: 1.前置过滤器 jQuery. ajaxPrefilter 2.请求分发器 jQuery. ajaxTransport, 3.类型转换器 ajaxConvert 源码结构: jQuery.extend({ /** * 前置过滤器 * @type {[type]} */ ajaxPrefilter: addToPrefiltersOrTransports(prefilters), /** * 请求分发器 *

jQuery源码学习感想

还记得去年(2015)九月份的时候,作为一个大四的学生去参加美团霸面,结果被美团技术总监教育了一番,那次问了我很多jQuery源码的知识点,以前虽然喜欢研究框架,但水平还不足够来研究jQuery源码,那时我不明白他们为何要求那么高,现在才知道,原来没那么高,他问的都是jQuery最基本的框架架构,不过对于不知道的来说,再简单我也是不知道,那时写了一篇博文去吐槽了一下,那时候也是我自己真正激发自己的时候,那时候我说我一定要搞好自己的jQuery基础,没想到那么快就实现了,一个月的源码学习时间就结束

jquery源码分析(二)——结构

再来复习下整体架构: jQuery源码分析(基于 jQuery 1.11 版本,共计8829行源码) (21,94)                定义了一些变量和函数jQuery=function(){} (96,280)        给jQuery添加一些方法和属性,jQuery.fn=jQuery.prototype(285,347)        extend:        jQuery的一些继承方法        更容易进行后续的扩展                       

jQuery源码学习2——初始化篇

这一篇主要总结一下jQuery这个js在引入的时候做的一些初始化工作 第一句window.undefined=window.undefined; 是为了兼容低版本的IE而写的 因为在低版本的IE中undefined不是window对象下的属性 因此window.undefined就是undefined 根据=运算符右结核性的特征,=右边的window.undefined就是undefined 既然window没有undefined属性 因此左边其实可以理解为在window下面扩展一个undefi

jquery 源码学习(四)构造jQuery对象-工具函数

jQuery源码分析-03构造jQuery对象-工具函数,需要的朋友可以参考下. 作者:nuysoft/高云 QQ:47214707 EMail:[email protected] 声明:本文为原创文章,如需转载,请注明来源并保留原文链接. 读读写写,不对的地方请告诉我,多多交流共同进步,本章的的PDF等本章写完了发布. jQuery源码分析系列的目录请查看 http://nuysoft.iteye.com/blog/1177451,想系统的好好写写,目前还是从我感兴趣的部分开始,如果大家有对哪

jQuery源码分析-jQuery中的循环技巧

Js代码   作者:nuysoft/JS攻城师/高云 QQ:47214707 EMail:[email protected] 声明:本文为原创文章,如需转载,请注明来源并保留原文链接. 前记:本文收集了jQuery中出现的各种遍历技巧和场景 Js代码   // 简单的for-in(事件) for ( type in events ) { } Js代码   // 缓存length属性,避免每次都去查找length属性,稍微提升遍历速度 // 但是如果遍历HTMLCollection时,性能提升非常

二.jQuery源码解析之构建jQuery之构建函数jQuery的7种用法

一:$(selectorStr[,限制范围]),接受一个选择器(符合jQuery规范的字符串),返回一个jQuery对象;二:$(htmlStr[,文档对象]),$(html[,json对象])传入html字符串,创建一个新的dom元素 三:$(dom元素),$(dom元素集合)将dom元素转换成jQuery对象.四:$(自定义对象)封装普通对象为jQuery对象.五:$(回调函数)绑定ready事件监听函数,当Dom加载完成时执行.六:$(jQuery对象)接受一个jQuery对象,返回一个j