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
Nov 25 Javascript
jQuery中live()方法用法实例
Jan 19 Javascript
Laydate时间组件在火狐浏览器下有多时间输入框时只能给第一个输入框赋值的解决方法
Aug 18 Javascript
bootstrap table单元格新增行并编辑
May 19 Javascript
Javascript实现base64的加密解密方法示例
Jun 27 Javascript
详解用node搭建简单的静态资源管理器
Aug 09 Javascript
JS简单实现查看文档创建日期、修改日期和文档大小的方法示例
Apr 08 Javascript
vue-cli项目根据线上环境分别打出测试包和生产包
May 23 Javascript
laravel-admin 与 vue 结合使用实例代码详解
Jun 04 Javascript
在vue中获取wangeditor的html和text的操作
Oct 23 Javascript
vue3使用vue-count-to组件的实现
Dec 25 Vue.js
原生JS运动实现轮播图
Jan 02 Javascript
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
phpMyAdmin2.11.6安装配置方法
2008/08/24 PHP
PHP依赖倒置(Dependency Injection)代码实例
2014/10/11 PHP
PHP date()格式MySQL中插入datetime方法
2019/01/29 PHP
js进行表单验证实例分析
2015/02/10 Javascript
JS自动倒计时30秒后按钮才可用(两种场景)
2015/08/31 Javascript
JS开发中百度地图+城市联动实现实时触发查询地址功能
2017/04/13 Javascript
Vue组件之Tooltip的示例代码
2017/10/18 Javascript
vue-router配合ElementUI实现导航的实例
2018/02/11 Javascript
layer弹出的iframe层在执行完毕后关闭当前弹出层的方法
2018/08/17 Javascript
微信小程序模板消息推送的两种实现方式
2019/08/27 Javascript
Vue-CLI 3 scp2自动部署项目至服务器的方法
2020/07/24 Javascript
js实现可爱的气泡特效
2020/09/05 Javascript
[40:13]Ti4 冒泡赛第二天 iG vs NEWBEE 2
2014/07/15 DOTA
python3使用tkinter实现ui界面简单实例
2014/01/10 Python
python将图片文件转换成base64编码的方法
2015/03/14 Python
浅谈Python中chr、unichr、ord字符函数之间的对比
2016/06/16 Python
python+django+sql学生信息管理后台开发
2018/01/11 Python
运行django项目指定IP和端口的方法
2018/05/14 Python
python+tkinter实现学生管理系统
2019/08/20 Python
Python目录和文件处理总结详解
2019/09/02 Python
使用python实现kNN分类算法
2019/10/16 Python
Jupyter加载文件的实现方法
2020/04/14 Python
Python爬虫定时计划任务的几种常见方法(推荐)
2021/01/15 Python
python中K-means算法基础知识点
2021/01/25 Python
HQhair美国/加拿大:英国化妆品、美容及美发产品商城
2019/04/15 全球购物
迪士尼法国在线商店:shopDisney FR
2020/12/03 全球购物
医院后勤自我鉴定
2013/10/13 职场文书
实习鉴定评语
2014/01/19 职场文书
《我的信念》教学反思
2014/02/15 职场文书
班级道德讲堂实施方案
2014/02/24 职场文书
英语教育专业自荐信
2014/05/29 职场文书
商场消防安全责任书
2014/07/29 职场文书
2015年英语教学工作总结
2015/05/25 职场文书
卫生主题班会
2015/08/14 职场文书
Redis IP地址的绑定的实现
2021/05/08 Redis
自己搭建resnet18网络并加载torchvision自带权重的操作
2021/05/13 Python