jQuery-1.9.1源码分析系列(十)事件系统之事件体系结构


Posted in Javascript onNovember 19, 2015

又是一个重磅功能点。

在分析源码之前分析一下体系结构,有助于源码理解。实际上在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);
 }
};

 重新梳理一下数据结构,使用一个例子

<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;
}

依然用实例来说明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的事件结构就清楚了。后面再分析事件的绑定和触发以及委托原理。

Javascript 相关文章推荐
JavaScript 实现??打印?理
Apr 28 Javascript
Iframe 自动适应页面的高度示例代码
Feb 26 Javascript
JavaScript中几种排序算法的简单实现
Jul 29 Javascript
js实现浏览本地文件并显示扩展名的方法
Aug 17 Javascript
基于jQuery实现仿QQ空间送礼物功能代码
May 24 Javascript
轻松掌握jQuery中wrap()与unwrap()函数的用法
May 24 Javascript
基于JS实现checkbox全选功能实例代码
Oct 31 Javascript
JS新包管理工具yarn和npm的对比与使用入门
Dec 09 Javascript
Angular中响应式表单的三种更新值方法详析
Aug 22 Javascript
React-native桥接Android原生开发详解
Jan 17 Javascript
Vue使用Clipboard.JS在h5页面中复制内容实例详解
Sep 03 Javascript
js简单的分页器插件代码实例
Sep 11 Javascript
javascript电商网站抢购倒计时效果实现
Nov 19 #Javascript
跟我学习javascript的Date对象
Nov 19 #Javascript
跟我学习javascript的this关键字
May 28 #Javascript
jQuery 1.9.1源码分析系列(十)事件系统之绑定事件
Nov 19 #Javascript
基于Jquery代码实现手风琴菜单
Nov 19 #Javascript
跟我学习javascript的作用域与作用域链
Nov 19 #Javascript
每天一篇javascript学习小结(属性定义方法)
Nov 19 #Javascript
You might like
php中使用ExcelFileParser处理excel获得数据(可作批量导入到数据库使用)
2010/08/21 PHP
应用开发中涉及到的css和php笔记分享
2011/08/02 PHP
如何使用php判断服务器是否是HTTPS连接
2013/07/05 PHP
一个基于phpQuery的php通用采集类分享
2014/04/09 PHP
PHP基于SMTP协议实现邮件发送实例代码
2017/04/27 PHP
PHP实现的贪婪算法实例
2017/10/17 PHP
Laravel如何使用Redis共享Session
2018/02/23 PHP
js 绑定带参数的事件以及手动触发事件
2010/04/27 Javascript
jQuery EasyUI API 中文文档 - TimeSpinner时间微调器
2011/10/23 Javascript
node.js中的fs.openSync方法使用说明
2014/12/17 Javascript
JavaScript动态修改背景颜色的方法
2015/04/16 Javascript
微信支付如何实现内置浏览器的H5页面支付
2015/09/25 Javascript
JavaScript中关联原型链属性特性
2016/02/13 Javascript
Node.js+Express配置入门教程详解
2016/05/19 Javascript
NodeJS中的MongoDB快速入门详细教程
2016/11/11 NodeJs
jQuery实现两列等高并自适应高度
2016/12/22 Javascript
JavaScript闭包原理与用法实例分析
2018/08/10 Javascript
Vuex的基本概念、项目搭建以及入坑点
2018/11/04 Javascript
[47:53]DOTA2上海特级锦标赛主赛事日 - 1 败者组第一轮#2COL VS Spirit
2016/03/02 DOTA
用Django实现一个可运行的区块链应用
2018/03/08 Python
python opencv旋转图像(保持图像不被裁减)
2018/07/26 Python
详解关于Django中ORM数据库迁移的配置
2018/10/08 Python
详解Django的model查询操作与查询性能优化
2018/10/16 Python
Python FtpLib模块应用操作详解
2019/12/12 Python
Python编程快速上手——Excel表格创建乘法表案例分析
2020/02/28 Python
Python Charles抓包配置实现流程图解
2020/09/29 Python
澳大利亚药房在线:ThePharmacy
2017/10/04 全球购物
Book Depository美国:全球领先的专业网上书店之一
2019/08/14 全球购物
医学专业个人求职自荐信格式
2013/09/23 职场文书
毕业生求职找工作的自我评价范文
2013/11/27 职场文书
竞选演讲稿范文
2013/12/28 职场文书
妇女干部培训方案
2014/05/12 职场文书
个人四风问题对照检查材料
2014/10/01 职场文书
冬季作息时间调整通知
2015/04/24 职场文书
导游词之上饶龟峰
2019/10/25 职场文书
BCL经典机 SONY ICF-5900W电路分析
2022/04/24 无线电