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 TO HTML 转换
Jun 26 Javascript
去除链接虚线全面分析总结
Aug 15 Javascript
Javascript 设计模式(二) 闭包
May 26 Javascript
JavaScript实现文本框中默认显示背景图片在获得焦点后消失的方法
Jul 01 Javascript
Bootstrap实现下拉菜单效果
Apr 29 Javascript
让编辑器支持word复制黏贴、截屏的js代码
Oct 17 Javascript
基于jQuery实现的打字机效果
Jan 16 Javascript
Jquery鼠标放上去显示全名的实现方法
Feb 06 Javascript
Vue 项目部署到服务器的问题解决方法
Dec 05 Javascript
vue实现购物车列表
Jun 30 Javascript
在vue中实现嵌套页面(iframe)
Jul 30 Javascript
vue使用echarts实现折线图
Mar 21 Vue.js
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插件 HTMLPurifier HTML解析器
2013/07/01 PHP
php导出CSV抽象类实例
2014/09/24 PHP
php5.3不能连接mssql数据库的解决方法
2014/12/27 PHP
PHP命名空间与自动加载机制的基础介绍
2019/08/25 PHP
php设计模式之中介者模式分析【星际争霸游戏案例】
2020/03/23 PHP
use jscript with List Proxy Server Information
2007/06/11 Javascript
js 模拟气泡屏保效果代码
2010/07/10 Javascript
jquery动画2.元素坐标动画效果(创建一个图片走廊)
2012/08/24 Javascript
window.location.href = window.location.href 跳转无反应 a超链接onclick事件写法
2013/08/21 Javascript
jQuery实现跨域iframe接口方法调用
2015/03/14 Javascript
比较常见的javascript中定义函数的区别
2015/11/09 Javascript
AngularJs bootstrap详解及示例代码
2016/09/01 Javascript
jQuery实现带延时功能的水平多级菜单效果【附demo源码下载】
2016/09/21 Javascript
简单实现bootstrap导航效果
2017/02/07 Javascript
webpack构建react多页面应用详解
2017/09/15 Javascript
React通过父组件传递类名给子组件的实现方法
2017/11/13 Javascript
基于vue-ssr的静态网站生成器VuePress 初体验
2018/04/17 Javascript
js实现消灭星星(web简易版)
2020/03/24 Javascript
[51:43]OG vs LGD 2018国际邀请赛淘汰赛BO3 第五场 8.26
2018/08/30 DOTA
Python 文件管理实例详解
2015/11/10 Python
python使用锁访问共享变量实例解析
2018/02/08 Python
python3实现弹弹球小游戏
2019/11/25 Python
Python下划线5种含义代码实例解析
2020/07/10 Python
css3一款3D字体带阴影效果的实现步骤
2013/03/20 HTML / CSS
CSS3的calc()做响应模式布局的实现方法
2017/09/06 HTML / CSS
荷兰度假屋租赁网站:Aan Zee
2020/02/28 全球购物
预备党员党课思想汇报
2014/01/13 职场文书
交通事故检查书范文
2014/01/30 职场文书
大学毕业自我评价
2014/02/02 职场文书
财务会计自荐信范文
2014/02/21 职场文书
2019求职信大礼包
2019/05/15 职场文书
2019XX公司员工考核管理制度!
2019/08/07 职场文书
CSS3 天气图标动画效果
2021/04/06 HTML / CSS
Python re.sub 反向引用的实现
2021/07/07 Python
《吸血鬼幸存者》新内容发布 追加多个全新模式
2022/04/07 其他游戏
Mysql调整优化之四种分区方式以及组合分区
2022/04/13 MySQL