zoukankan      html  css  js  c++  java
  • jQuery-1.9.1源码分析系列(十) 事件系统——事件体系结构

      又是一个重磅功能点。

      在分析源码之前分析一下体系结构,有助于源码理解。实际上在jQuery出现之前,Dean Edwards的跨浏览器AddEvent()设计做的已经比较优秀了;而且jQuery事件系统的设计思想也是基于该思想的,所以我们先分析一下Dean Edwards前辈的事件绑定。

    a. jQuery事件原型——Dean Edwards的跨浏览器AddEvent()设计


      源码解读

    //事件添加方法
    function addEvent(element, type, handler) {
      //保证每个不同的事件响应函数只有唯一一个id
        if (!handler.$$guid) handler.$$guid = addEvent.guid++;
     
      // 给element维护一个events属性,初始化为一个空对象。  
        // element.events的结构类似于 { "click": {...}, "dbclick": {...}, "change": {...} }  
        if (!element.events) element.events = {};
     
      // 试图取出element.events中当前事件类型type对应的对象(这个对象更像数组),赋值给handlers
      //如果element.events中没有当前事件类型type对应的对象则初始化
        var handlers = element.events[type];
      if (!handlers) {
         handlers = element.events[type] = {};
     
         // 如果这个element已经有了一个对应的事件的响应方法,例如已经有了onclick方法
            // 就把element的onclick方法赋值给handlers的0元素,此时handlers的结构就是:
            // { 0: function(e){...} },这也是为什么addEvent.guid初始化为1的原因,预留看为0的空间;
            // 此时element.events的结构就是: { "click": { 0: function(e){...} },  /*省略其他事件类型*/ } 
            if (element["on" + type]) {
               handlers[0] = element["on" + type];
            }
        }
        
      // 把当前的事件handler存放到handlers中,handler.$$guid = addEvent.guid++; addEvent.guid = 1; 肯定是从1开始累加的    
      //因此,这是handlers的结构可能就是 { 0: function(e){...}, 1: function(){}, 2: function(){} 等等... }
        handlers[handler.$$guid] = handler;
     
      //下文定义了一个handleEvent(event)函数,将这个函数,绑定到element的type事件上作为事件入口。
      //说明:在element进行click时,将会触发handleEvent函数,handleEvent函数将会查找element.events,并调用相应的函数。可以把handleEvent称为“主监听函数”
        element["on" + type] = handleEvent;
    };
     
    //计数器
    addEvent.guid = 1;
     
    function removeEvent(element, type, handler) {
        // delete the event handler from the hash table
        if (element.events && element.events[type]) {
            delete element.events[type][handler.$$guid];
        }
    };
     
    function handleEvent(event) {
      //兼容ie
      event = event || window.event;
      //this是响应事件的节点,这个接点上有events属性(在addEvent中添加的)
      //获取节点对应事件响应函数列表
        var handlers = this.events[event.type];
        // 循环响应函数列表执行
      for (var i in handlers) {
             //保持正确的作用域,即this关键字
         this.$$handleEvent = handlers[i];
            this.$$handleEvent(event);
        }
    };
    View Code
    重新梳理一下数据结构,使用一个例子
    <input type="text" id="chua" onClick="f0();">
    
    function f0(){...}
    function f1(){...}
    function f2(){...}
    function f3(){...}
    var dom = document.getElementById("chua");
    
    addEvent(dom,"click",f1);
    addEvent(dom,"change",f1);
    addEvent(dom,"change",f2);
    addEvent(dom,"click",f3);
    addEvent(dom,"change",f3);

      经过addEvent()函数之后,当前的数据结构为:

    element: {
      onclick: handleEvent(event),   //click事件的主监听函数
      onchage: handleEvent(event),  //change事件的主监听函数
      events: {     click:{//这是一个类数组       0: f0, //element已有的事件       1: f1,  //下标1实际上就是f1.$$guid       3: f3 //下标3实际上就是f3.$$guid,需要注意的是每一个响应事件都有一个唯一的$$guid作为下标       ...     },     change:{//这是一个类数组       1: f1,
          2: f2,
          3: f3     }
    } }

      事件系统会根据调用addEvent的顺序给每个响应函数(也就是addEvent(element, type, handler)中的第三个参数handler)打上标记$$guid。源码

      //保证每个不同的事件响应函数只有唯一一个id
        if (!handler.$$guid) handler.$$guid = addEvent.guid++;

      最终三个响应函数的$$guid标记分别是

      f1.$$guid = 1

      f2.$$guid = 2

      f3.$$guid = 3

      而根据源码中

        handlers[handler.$$guid] = handler;

      那么某一个函数在任何事件响应函数集合中的下标位置是固定的。比如click和change事件都调用f3作为响应事件,那么f3在element.events.click以及element.events.change中的下标位置都是f3.$$guid = 3;即element.events.click[3] = element.events.change[3] = f3。

      这个时候假设又新添了一个事件绑定:addEvent(dom,"focus",f3);那么element.events.focus[3] = f3;这也是对象相比于数组的方便之处,数组不可能没有下标0,1,2就直接有3了,但是对象却可以,此时3是作为对象的一个属性名称

      这样的设计,其实已经具备了jquery事件系统的雏形,包含了几个最主要的特点:

      1)element上的所有事件,将保存到element.events属性中,不是直接绑定到element上;这样一个事件可以有无数个响应函数。

      2)handleEvent作为element所有事件的“主监听函数”,有它统一管理element上的所有函数。

      3)所有浏览器都支持element["on" + type]事件绑定方式,跨浏览器兼容。

      

      好啦,明白了addEvent的事件结构,这个想法确实让人觉得眼前一亮。下面分析jQuery的事件结构

    b. jQuery的事件结构


      所有的函数添加事件都会进入jQuery.event.add函数。该函数有两个主要功能:添加事件、附加很多事件相关信息。我们直接上源码,源码思想和Dean Edwards的跨浏览器兼容事件添加处理类似。

      源码分析

    add: function( elem, types, handler, data, selector ) {
        var tmp, events, t, handleObjIn,
            special, eventHandle, handleObj,
            handlers, type, namespaces, origType,
            //获取elem节点对应的缓存数据
            elemData = jQuery._data( elem );
    
        //没有数据或文本/注释节点不能附加事件(但是允许附加普通对象)
        if ( !elemData ) {
            return;
        }
    
        //调用者能通过自定义数据替换handler
        if ( handler.handler ) {
            handleObjIn = handler;
            handler = handleObjIn.handler;
            selector = handleObjIn.selector;
        }
    
        //确保handler函数有唯一的ID,后续会用来查找/删除这个handler函数
        if ( !handler.guid ) {
            handler.guid = jQuery.guid++;
        }
    
        //如果是初次进入,初始化元素的事件结构和主事件响应入口
        if ( !(events = elemData.events) ) {
            events = elemData.events = {};
        }
        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;
        }
    
        //多个事件使用空格隔开的处理
      //如jQuery(...).bind("mouseover mouseout", fn);
      //core_rnotwhite = /S+/g;匹配空白字符
      types = ( types || "" ).match( core_rnotwhite ) || [""];
      t = types.length;
      while ( t-- ) {
            //rtypenamespace = /^([^.]*)(?:.(.+)|)$/;
            //获取命名空间和原型事件
            tmp = rtypenamespace.exec( types[t] ) || [];
            type = origType = tmp[1];
            namespaces = ( tmp[2] || "" ).split( "." ).sort();
    
            //如果事件改变其类型,使用special事件处理器来处理更改后的事件类型
            special = jQuery.event.special[ type ] || {};
    
            //如果选择器已定义,确定special事件API类型,否则给他一个类型
            type = ( selector ? special.delegateType : special.bindType ) || type;
    
            //基于新设置的类型更新special
            special = jQuery.event.special[ type ] || {};
    
            // handleObj贯穿整个事件处理
            handleObj = jQuery.extend({
                type: type,
                origType: origType,
                data: data,
                handler: handler,
                guid: handler.guid,
                selector: selector,
                // For use in libraries implementing .is(). We use this for POS matching in `select`
                //"needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\(" +
                //whitespace + "*((?:-\d)?\d*)" + whitespace + "*\)|)(?=[^-]|$)", "i" )
                //用来判断亲密关系
                needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
                namespace: namespaces.join(".")
            }, handleObjIn );
    
            //初次使用时初始化事件处理器队列
            if ( !(handlers = events[ type ]) ) {
                handlers = events[ type ] = [];
                handlers.delegateCount = 0;
    
                //非自定义事件,如果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 );
                    }
                }
            }
            //自定义事件绑定
            if ( special.add ) {
                special.add.call( elem, handleObj );
    
                if ( !handleObj.handler.guid ) {
                    handleObj.handler.guid = handler.guid;
                }
            }
    
            //将事件对象handleObj添加到元素的处理列表,代理计数递增
            if ( selector ) {
                handlers.splice( handlers.delegateCount++, 0, handleObj );
            } else {
                handlers.push( handleObj );
            }
    
            //跟踪那个事件曾经被使用过,用于事件优化
            jQuery.event.global[ type ] = true;
        }
    
        //防止ie内存泄漏
        elem = null;
    }
    View Code

      依然用实例来说明jQuery的事件结构

    <div id="#center"></div>
    
    <script>
      function dohander(){console.log("dohander")};
      function dot(){console.log("dot");}
    
      $(document).on("click",'#center',dohander)
      .on("click",'#center',dot)
      .on("click",dot);
    </script>

      经过添加处理环节,事件添加到了元素上,而且节点对应的缓存数据也添加了相应的数据。结构如下

    elemData = jQuery._data( elem );
    elemData = {
      events: {
        click: {//Array[3]
          0: {
            data: undefined/{...},
            guid: 2, //处理函数的id
            handler: function dohander(){…},
            namespace: "",
            needsContext: false,
            origType: "click",
            selector: "#center",//选择器,用来区分不同事件源
            type: "click"
          }
          1: {
            data: undefined/{...},
            guid: 3,
            handler: function dot(){…},
            namespace: "",
            needsContext: false,
            origType: "click",
            selector: "#center",
            type: "click"
          }
          2: {
            data: undefined,
            guid: 3,
            handler: function dot(){…},
            namespace: "",
            needsContext: false,
            origType: "click",
            selector: undefined,
            type: "click"
          }
          delegateCount: 2,//委托事件数量,有selector的才是委托事件
          length: 3
        }
      }
      handle: function ( e ) {…}/*事件处理主入口*/{
        elem: document//属于handle对象的特征
      }
    }

      jQuery的处理和Dean Edwards的跨浏览器兼容事件添加处理类似,比如为每一个函数添加guid;使用events对象存放响应事件列表,有一个总的事件处理入口handle等。

      jQuery做了哪些改进?

      1)事件数据不再直接保存在节点上,而是使用jQuery缓存系统内(内部使用的缓存jQuery._data方式存取)

      2)事件委托:绑定到当前节点(例子中当前节点是document根节点)的处理函数不仅仅包含当前节点触发事件(click)响应时处理的事件(例子中selector为undefined时对应的处理函数dot);还代理了其他节点(例子中的#center节点)触发事件(click)响应时处理的事件(例子中selector为"#center"对应的处理事件doHandler和dot);委托机制在后续分析。

      3)增加了很多功能数据,比如命名空间namespace:这个主要用在自定义事件自定义触发,比如$(document).on("chua.click",'#center',dot),主动触发$("#center").trigger("chua.click")。还有额外数据data:虽然没有看到那个地方有被用到。

      到此jQuery的事件结构就清楚了。后面再分析事件的绑定和触发以及委托原理。

  • 相关阅读:
    js项目结合的注意点
    cookie练习
    json记载字符个数
    js,jQuery获取标签
    新人签到
    使用Resources类搭建Unity简单的资源管理工具类
    初识vue
    原生JS写出贪吃蛇
    用js做一个简单的班级点名器
    Javascript 的"循环语句"
  • 原文地址:https://www.cnblogs.com/chuaWeb/p/jQuery-1-9-1-addEvents-1.html
Copyright © 2011-2022 走看看