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 相关文章推荐
JavaScript.The.Good.Parts阅读笔记(二)作用域&amp;闭包&amp;减缓全局空间污染
Nov 16 Javascript
js正则表达式的使用详解
Jul 09 Javascript
简单的代码实现jquery定时器
Jan 03 Javascript
JS判断、校验MAC地址的2个实例
May 05 Javascript
javascript html实现网页版日历代码
Mar 08 Javascript
用JS中split方法实现彩色文字背景效果实例
Aug 24 Javascript
jQuery和CSS仿京东仿淘宝列表导航菜单
Jan 04 Javascript
详解Angular中的自定义服务Service、Provider以及Factory
Apr 22 Javascript
使用express获取微信小程序二维码小记
May 21 Javascript
JavaScript使用表单元素验证表单的示例代码
Aug 20 Javascript
如何编写一个 Webpack Loader的实现
Oct 18 Javascript
vue使用lodop打印控件实现浏览器兼容打印的方法
Feb 07 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绘制一条直线的方法
2015/01/24 PHP
Yii使用技巧大汇总
2015/12/29 PHP
php使用ffmpeg获取视频信息并截图的实现方法
2016/05/03 PHP
php安装php_rar扩展实现rar文件读取和解压的方法
2016/11/17 PHP
js由下向上不断上升冒气泡效果实例
2015/05/07 Javascript
JS弹出窗口插件zDialog简单用法示例
2016/06/12 Javascript
把普通对象转换成json格式的对象的简单实例
2016/07/04 Javascript
jQuery延迟执行的实现方法
2016/12/21 Javascript
原生js实现放大镜效果
2017/01/11 Javascript
微信小程序 仿美团分类菜单 swiper分类菜单
2017/04/12 Javascript
Vuex之理解Getters的用法实例
2017/04/19 Javascript
详解vue-cli本地环境API代理设置和解决跨域
2017/09/05 Javascript
详解Ant Design of React的安装和使用方法
2018/12/27 Javascript
webpack4实现不同的导出类型
2019/04/09 Javascript
解决Layui数据表格的宽高问题
2019/09/28 Javascript
基于vue-cli3和element实现登陆页面
2019/11/13 Javascript
vue使用lodop打印控件实现浏览器兼容打印的方法
2021/02/07 Vue.js
Python爬虫利用cookie实现模拟登陆实例详解
2017/01/12 Python
Python队列的定义与使用方法示例
2017/06/24 Python
Python使用django框架实现多人在线匿名聊天的小程序
2017/11/29 Python
python感知机实现代码
2019/01/18 Python
Python3利用Dlib实现摄像头实时人脸检测和平铺显示示例
2019/02/21 Python
值得收藏的10道python 面试题
2019/04/15 Python
python logging添加filter教程
2019/12/24 Python
Python逐行读取文件内容的方法总结
2020/02/14 Python
python中entry用法讲解
2020/12/04 Python
无需JS和jQuery代码实现CSS3鼠标浮动放大图片
2016/11/21 HTML / CSS
h5实现获取用户地理定位的实例代码
2017/07/17 HTML / CSS
印度尼西亚最完整和最大的在线药房网站:Farmaku.com
2019/11/23 全球购物
2013年入党人员的自我鉴定
2013/10/25 职场文书
授权委托书格式模板
2014/04/03 职场文书
保密工作整改情况汇报
2014/11/06 职场文书
作文批改评语
2014/12/25 职场文书
离婚案件原告代理词
2015/05/23 职场文书
「我的青春恋爱物语果然有问题。-妄言录-」第20卷封面公开
2022/03/21 日漫
Java中的Kafka为什么性能这么快及4大核心详析
2022/09/23 Java/Android