jquery事件绑定解绑机制源码解析


Posted in Javascript onSeptember 19, 2016

引子

为什么Jquery能实现不传回调函数也能解绑事件?如下:

$("p").on("click",function(){
  alert("The paragraph was clicked.");
});

$("#box1").off("click");

事件绑定解绑机制

调用on函数的时候,将生成一份事件数据,结构如下:

{
  type: type,
  origType: origType,
  data: data,
  handler: handler,
  guid: guid,
  selector: selector,
  needsContext: needsContext,
  namespace: namespace
}

并将该数据加入到元素的缓存中。jquery中每个元素都可以有一个缓存(只有有需要的时候才生成),其实就是该元素的一个属性。jquery为每个元素的每种事件都建立一个队列,用来保存事件处理函数,所以可以对一个元素添加多个事件处理函数。缓存的结构如下:

"div#box":{ //元素
  "Jquery623873":{ //元素的缓存
    "events":{ 
      "click":[
       {  //元素click事件的事件数据
             type: type,
             origType: origType,
             data: data,
             handler: handler,
             guid: guid,
             selector: selector,
             needsContext: needsContext,
             namespace: namespace
               }
      ],
      "mousemove":[
       {
             type: type,
             origType: origType,
             data: data,
             handler: handler,
             guid: guid,
             selector: selector,
             needsContext: needsContext,
             namespace: namespace
               }
      ]
    }
  }
}

当要解绑事件的时候,如果没指定fn参数,jquery就会从该元素的缓存里拿到要解绑的事件的处理函数队列,从里面拿出fn参数,然后调用removeEventListener进行解绑。

源代码

代码注释可能不太清楚,可以复制出来看

jquery原型中的on,one,off方法:

事件绑定从这里开始

jQuery.fn.extend( {

  on: function( types, selector, data, fn ) {
    return on( this, types, selector, data, fn );
  },
  one: function( types, selector, data, fn ) {
    return on( this, types, selector, data, fn, 1 );
  },
  off: function( types, selector, fn ) {

    //此处省略处理参数的代码

    return this.each( function() {
      jQuery.event.remove( this, types, fn, selector );
    } );
  }
} );

独立出来供one和on调用的on函数:

function on( elem, types, selector, data, fn, one ) {
  var origFn, type;

  //此处省略处理参数的代码

  //是否是通过one绑定,是的话使用一个函数代理当前事件回调函数,代理函数只执行一次
  //这里使用到了代理模式
  if ( one === 1 ) {   
    origFn = fn;
    fn = function( event ) {

      // Can use an empty set, since event contains the info
      jQuery().off( event );
      return origFn.apply( this, arguments );
    };

    // Use same guid so caller can remove using origFn
    fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
  }

  /************************************************
  *** jquery将所有选择到的元素到放到一个数组里,然后
  *** 对每个元素到使用event对象的add方法绑定事件
  *************************************************/
  return elem.each( function() {
    jQuery.event.add( this, types, fn, data, selector );
  } );
}

处理参数的代码也可以看一下,实现on("click",function(){})这样调用 on:function(types, selector, data, fn)也不会出错。其实就是内部判断,如果data, fn参数为空的时候,把selector赋给fn 

event对象是事件绑定的一个关键对象:

这里处理把事件绑定到元素和把事件信息添加到元素缓存的工作:

jQuery.event = {

  add: function( elem, types, handler, data, selector ) {

    var handleObjIn, eventHandle, tmp,
      events, t, handleObj,
      special, handlers, type, namespaces, origType,
      elemData = dataPriv.get( elem );  //这句将检查elem是否被缓存,如果没有将会创建一个缓存添加到elem元素上。形式诸如:elem["jQuery310057655476080253721"] = {}


    // Don't attach events to noData or text/comment nodes (but allow plain objects)
    if ( !elemData ) {
      return;
    }


    //用户可以传入一个自定义数据对象来代替事件回调函数,将事件回调函数放在这个数据对象的handler属性里
    if ( handler.handler ) {
      handleObjIn = handler;
      handler = handleObjIn.handler;
      selector = handleObjIn.selector;
    }

    //每个事件回调函数都会生成一个唯一的id,以后find/remove的时候会用到

    if ( !handler.guid ) {
      handler.guid = jQuery.guid++;
    }

    // 如果元素第一次绑定事件,则初始化元素的事件数据结构和主回调函数(main)
    //说明:每个元素有一个主回调函数,作为绑定多个事件到该元素时的回调的入口
    if ( !( events = elemData.events ) ) {
      events = elemData.events = {};
    }
    //这里就是初始化主回调函数的代码
    if ( !( eventHandle = elemData.handle ) ) {
      eventHandle = elemData.handle = function( e ) {

        // Discard the second event of a jQuery.event.trigger() and
        // when an event is called after a page has unloaded
        return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
          jQuery.event.dispatch.apply( elem, arguments ) : undefined;
      };
    }

    // 处理事件绑定,考虑到可能会通过空格分隔传入多个事件,这里要进行多事件处理
    types = ( types || "" ).match( rnotwhite ) || [ "" ];
    t = types.length;
    while ( t-- ) {
      tmp = rtypenamespace.exec( types[ t ] ) || []; 
      type = origType = tmp[ 1 ];
      namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort();

      // There *must* be a type, no attaching namespace-only handlers
      if ( !type ) {
        continue;
      }

      // If event changes its type, use the special event handlers for the changed type
      special = jQuery.event.special[ type ] || {};

      // If selector defined, determine special event api type, otherwise given type
      type = ( selector ? special.delegateType : special.bindType ) || type;

      // Update special based on newly reset type
      special = jQuery.event.special[ type ] || {};

      // 事件回调函数的数据对象
      handleObj = jQuery.extend( {
        type: type,
        origType: origType,
        data: data,
        handler: handler,
        guid: handler.guid,
        selector: selector,
        needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
        namespace: namespaces.join( "." )
      }, handleObjIn );

      // 加入第一次绑定该类事件,会初始化一个数组作为事件回调函数队列,每个元素的每一种事件有一个队列
      if ( !( handlers = events[ type ] ) ) {
        handlers = events[ type ] = [];
        handlers.delegateCount = 0;

        // Only use addEventListener if the special events handler returns false
        if ( !special.setup ||
          special.setup.call( elem, data, namespaces, eventHandle ) === false ) {

          if ( elem.addEventListener ) {
            elem.addEventListener( type, eventHandle );
          }
        }
      }

      if ( special.add ) {
        special.add.call( elem, handleObj );

        if ( !handleObj.handler.guid ) {
          handleObj.handler.guid = handler.guid;
        }
      }

      // 加入到事件回调函数队列
      if ( selector ) {
        handlers.splice( handlers.delegateCount++, 0, handleObj );
      } else {
        handlers.push( handleObj );
      }

      // Keep track of which events have ever been used, for event optimization
      // 用来追踪哪些事件从未被使用,用以优化
      jQuery.event.global[ type ] = true;
    }

  }
};

千万注意,对象和数组传的是引用!比如将事件数据保存到缓存的代码:

handlers = events[ type ] = [];

if ( selector ) {
  handlers.splice( handlers.delegateCount++, 0, handleObj );
} else {
  handlers.push( handleObj );
}

handlers的改变,events[ type ]会同时改变。

dataPriv就是管理缓存的对象:

其工作就是给元素创建一个属性,这个属性是一个对象,然后把与这个元素相关的信息放到这个对象里面,缓存起来。这样需要使用到这个对象的信息时,只要知道这个对象就可以拿到:

function Data() {
  this.expando = jQuery.expando + Data.uid++;
}

Data.uid = 1;

//删除部分没用到代码
Data.prototype = {

  cache: function( owner ) {

    // 取出缓存,可见缓存就是目标对象的一个属性
    var value = owner[ this.expando ];

    // 如果对象还没有缓存,则创建一个
    if ( !value ) {
      value = {};

      // We can accept data for non-element nodes in modern browsers,
      // but we should not, see #8335.
      // Always return an empty object.
      if ( acceptData( owner ) ) {

        // If it is a node unlikely to be stringify-ed or looped over
        // use plain assignment
        if ( owner.nodeType ) {
          owner[ this.expando ] = value;

        // Otherwise secure it in a non-enumerable property
        // configurable must be true to allow the property to be
        // deleted when data is removed
        } else {
          Object.defineProperty( owner, this.expando, {
            value: value,
            configurable: true
          } );
        }
      }
    }

    return value;
  },
  get: function( owner, key ) {
    return key === undefined ?
      this.cache( owner ) :

      // Always use camelCase key (gh-2257) 驼峰命名
      owner[ this.expando ] && owner[ this.expando ][ jQuery.camelCase( key ) ];
  },
  remove: function( owner, key ) {
    var i,
      cache = owner[ this.expando ];

    if ( cache === undefined ) {
      return;
    }

    if ( key !== undefined ) {

      // Support array or space separated string of keys
      if ( jQuery.isArray( key ) ) {

        // If key is an array of keys...
        // We always set camelCase keys, so remove that.
        key = key.map( jQuery.camelCase );
      } else {
        key = jQuery.camelCase( key );

        // If a key with the spaces exists, use it.
        // Otherwise, create an array by matching non-whitespace
        key = key in cache ?
          [ key ] :
          ( key.match( rnotwhite ) || [] );
      }

      i = key.length;

      while ( i-- ) {
        delete cache[ key[ i ] ];
      }
    }

    // Remove the expando if there's no more data
    if ( key === undefined || jQuery.isEmptyObject( cache ) ) {

      // Support: Chrome <=35 - 45
      // Webkit & Blink performance suffers when deleting properties
      // from DOM nodes, so set to undefined instead
      // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted)
      if ( owner.nodeType ) {
        owner[ this.expando ] = undefined;
      } else {
        delete owner[ this.expando ];
      }
    }
  },
  hasData: function( owner ) {
    var cache = owner[ this.expando ];
    return cache !== undefined && !jQuery.isEmptyObject( cache );
  }
};

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
excel操作之Add Data to a Spreadsheet Cell
Jun 12 Javascript
用javascript实现兼容IE7的类库 IE7_0_9.zip提供下载
Aug 08 Javascript
jquery的ajax()函数传值中文乱码解决方法介绍
Nov 08 Javascript
创建、调用JavaScript对象的方法集锦
Dec 24 Javascript
ExpressJS入门实例
Jan 14 Javascript
script标签属性用type还是language
Jan 21 Javascript
基于jQuery实现的菜单切换效果
Oct 16 Javascript
Easyui Treegrid改变默认图标的方法
Apr 29 Javascript
详解性能更优越的小程序图片懒加载方式
Jul 18 Javascript
vue-cli点击实现全屏功能
Mar 07 Javascript
VUE使用 wx-open-launch-app 组件开发微信打开APP功能
Aug 11 Javascript
vue中data改变后让视图同步更新的方法
Mar 29 Vue.js
JavaScript学习笔记整理_setTimeout的应用
Sep 19 #Javascript
Node.js + Redis Sorted Set实现任务队列
Sep 19 #Javascript
JavaScript学习笔记整理_用于模式匹配的String方法
Sep 19 #Javascript
JavaScript学习笔记整理_简单实现枚举类型,扑克牌应用
Sep 19 #Javascript
JavaScript学习笔记整理_关于表达式和语句
Sep 19 #Javascript
javascript学习笔记_浅谈基础语法,类型,变量
Sep 19 #Javascript
js中用cssText设置css样式的简单方法
Sep 19 #Javascript
You might like
PHP 编程的 5个良好习惯
2009/02/20 PHP
PHP的substr_replace将指定两位置之间的字符替换为*号
2011/05/04 PHP
Yii2框架数据验证操作实例详解
2018/05/02 PHP
PHP实现微信公众号验证Token的示例代码
2019/12/16 PHP
Javascript &amp; DHTML 实例编程(教程)DOM基础和基本API
2007/06/02 Javascript
XRegExp 0.2: Now With Named Capture
2007/11/30 Javascript
JavaScript 节点操作 以及DOMDocument属性和方法
2007/12/06 Javascript
深入理解javascript中return的作用
2013/12/30 Javascript
使用简洁的jQuery方法实现隔行换色功能
2014/01/02 Javascript
js修改原型的属性使用介绍
2014/01/26 Javascript
js时间比较示例分享(日期比较)
2014/03/05 Javascript
jQuery设置和获取HTML、文本和值示例
2014/07/08 Javascript
node.js不得不说的12点内容
2014/07/14 Javascript
JS数字抽奖游戏实现方法
2015/05/04 Javascript
在其他地方你学不到的jQuery小贴士和技巧(欢迎收藏)
2016/01/20 Javascript
JS使用eval()动态创建变量的方法
2016/06/03 Javascript
js实现背景图自适应窗口大小
2017/01/10 Javascript
nodejs入门教程六:express模块用法示例
2017/04/24 NodeJs
js实现音乐播放控制条
2017/09/09 Javascript
使用use注册Vue全局组件和全局指令的方法
2018/03/08 Javascript
vue的安装及element组件的安装方法
2018/03/09 Javascript
js异步上传多张图片插件的使用方法
2018/10/22 Javascript
vue 进阶之实现父子组件间的传值
2019/04/26 Javascript
vue点击按钮动态创建与删除组件功能
2019/12/29 Javascript
[03:58]2014DOTA2国际邀请赛 龙宝赛后解密DK获胜之道
2014/07/14 DOTA
python将pandas datarame保存为txt文件的实例
2019/02/12 Python
500行Python代码打造刷脸考勤系统
2019/06/03 Python
在linux下实现 python 监控usb设备信号
2019/07/03 Python
Python+Pyqt实现简单GUI电子时钟
2021/02/22 Python
HTML5+css3:3D旋转木马效果相册
2017/01/03 HTML / CSS
iHerb台湾:维生素、保健品和健康产品
2018/01/31 全球购物
购买中国最好的电子产品:Geekbuying
2018/03/13 全球购物
《毛主席在花山》教学反思
2014/04/20 职场文书
2014年国庆节广播稿
2014/09/19 职场文书
Python一行代码实现自动发邮件功能
2021/05/30 Python
Apache Pulsar集群搭建部署详细过程
2022/02/12 Servers