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 相关文章推荐
Node.js中对通用模块的封装方法
Jun 06 Javascript
jQuery简单实现图片预加载
Apr 20 Javascript
jquery Validation表单验证使用详解
Sep 12 Javascript
AngularJS 避繁就简的路由
Jul 01 Javascript
分享vue.js devtools遇到一系列问题
Oct 24 Javascript
jQuery实现页码跳转式动态数据分页
Dec 31 jQuery
JavaScript数组、json对象、eval()函数用法实例分析
Feb 21 Javascript
javascript实现手动点赞效果
Apr 09 Javascript
微信小程序开发注意指南和优化实践(小结)
Jun 21 Javascript
利用H5api实现时钟的绘制(javascript)
Sep 13 Javascript
如何在Express4.x中愉快地使用async的方法
Nov 18 Javascript
使用Mock.js生成前端测试数据
Dec 13 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 in_array 函数使用说明与in_array需要注意的地方说明
2010/04/13 PHP
PHP数组交集的优化代码分析
2011/03/06 PHP
解析mysql 表中的碎片产生原因以及清理
2013/06/22 PHP
PHP设计模式之适配器模式(Adapter)原理与用法详解
2019/12/12 PHP
浅谈javascript中的作用域
2012/04/07 Javascript
js或者jquery判断图片是否加载完成实现代码
2013/03/20 Javascript
解析Jquery取得iframe中元素的几种方法
2013/07/04 Javascript
IE下写xml文件的两种方式(fso/saveAs)
2013/08/05 Javascript
bootstrap data与jquery .data
2014/07/07 Javascript
JavaScript数据结构与算法之栈与队列
2016/01/29 Javascript
Highcharts入门之基本属性
2016/08/02 Javascript
Bootstrap基本插件学习笔记之Tooltip提示工具(18)
2016/12/08 Javascript
如何解决hover在ie6中的兼容性问题
2016/12/15 Javascript
Ajax高级笔记 JavaScript高级程序设计笔记
2017/06/22 Javascript
vue跳转同一个组件,参数不同,页面接收值只接收一次的解决方法
2019/11/05 Javascript
如何通过JS实现日历简单算法
2020/10/14 Javascript
python中定义结构体的方法
2013/03/04 Python
python连接mysql数据库示例(做增删改操作)
2013/12/31 Python
python基础教程之基本内置数据类型介绍
2014/02/20 Python
Python中文分词实现方法(安装pymmseg)
2016/06/14 Python
Python开发SQLite3数据库相关操作详解【连接,查询,插入,更新,删除,关闭等】
2017/07/27 Python
python实现RabbitMQ的消息队列的示例代码
2018/11/08 Python
python 实现图片上传接口开发 并生成可以访问的图片url
2019/12/18 Python
Opencv图像处理:如何判断图片里某个颜色值占的比例
2020/06/03 Python
早晨薰衣草在线女性精品店:Morning Lavender
2021/01/04 全球购物
中科软笔试题和面试题
2014/10/07 面试题
初任培训自我鉴定
2013/10/07 职场文书
幼儿园开学寄语
2014/04/03 职场文书
供货协议书
2014/04/22 职场文书
学生会竞选演讲稿学习部
2014/08/25 职场文书
出售房屋协议书范本
2014/10/06 职场文书
蓬莱阁导游词
2015/02/04 职场文书
html+css实现文字折叠特效实例
2021/06/02 HTML / CSS
浅谈MySQL 亿级数据分页的优化
2021/06/15 MySQL
Spring事务管理下synchronized锁失效问题的解决方法
2022/03/31 Java/Android
深入理解mysql事务隔离级别和存储引擎
2022/04/12 MySQL