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 相关文章推荐
加载列表时jquery获取ul中第一个li的属性
Nov 02 Javascript
jQuery插件Tmpl的简单使用方法
Apr 27 Javascript
JavaScript必知必会(二) null 和undefined
Jun 08 Javascript
Javascript中for循环语句的几种写法总结对比
Jan 23 Javascript
基于JS代码实现简单易用的倒计时 x 天 x 时 x 分 x 秒效果
Jul 13 Javascript
vue.js整合vux中的上拉加载下拉刷新实例教程
Jan 09 Javascript
jQuery+datatables插件实现ajax加载数据与增删改查功能示例
Apr 17 jQuery
vue获取元素宽、高、距离左边距离,右,上距离等还有XY坐标轴的方法
Sep 05 Javascript
解决layer弹出层中表单不起作用的问题
Sep 09 Javascript
解决layui弹出层layer的area过大被遮挡的问题
Sep 21 Javascript
在Angular中实现一个级联效果的下拉框的示例代码
May 20 Javascript
Vue2.x和Vue3.x的双向绑定原理详解
Nov 05 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
discuz 首页四格:最新话题+最新回复+热门话题+精华文章插件
2007/08/19 PHP
php开发留言板的CRUD(增,删,改,查)操作
2012/04/19 PHP
LotusPhp笔记之:基于ObjectUtil组件的使用分析
2013/05/06 PHP
php的SimpleXML方法读写XML接口文件实例解析
2014/06/16 PHP
Symfony2安装的方法(2种方法)
2016/02/04 PHP
php 伪造HTTP_REFERER页面URL来源的三种方法
2016/09/22 PHP
浅谈PHP错误类型及屏蔽方法
2017/05/27 PHP
Javascript中的相等与不等运算
2010/04/25 Javascript
brook javascript框架介绍
2011/10/10 Javascript
php图像生成函数之间的区别分析
2012/12/06 Javascript
跟我学Nodejs(三)--- Node.js模块
2014/05/25 NodeJs
基于zepto的移动端轻量级日期插件--date_picker
2016/03/04 Javascript
jQuery实现遍历复选框的方法示例
2017/03/06 Javascript
JS中type=&quot;button&quot;和type=&quot;submit&quot;的区别
2017/07/04 Javascript
jQuery事件_动力节点Java学院整理
2017/07/05 jQuery
Angular.js通过自定义指令directive实现滑块滑动效果
2017/10/13 Javascript
webpack+vue+express(hot)热启动调试简单配置方法
2018/09/19 Javascript
原生JS实现顶部导航栏显示按钮+搜索框功能
2019/12/25 Javascript
[01:38]完美世界DOTA2联赛(PWL)宣传片:第一站
2020/10/26 DOTA
python标准日志模块logging的使用方法
2013/11/01 Python
Python下载指定页面上图片的方法
2016/05/12 Python
浅谈pandas中DataFrame关于显示值省略的解决方法
2018/04/08 Python
python3 kmp 字符串匹配的方法
2018/07/07 Python
Python中xml和json格式相互转换操作示例
2018/12/05 Python
jupyter notebook 重装教程
2020/04/16 Python
定义css设备类型-Media Queries图表简介及使用方法
2013/01/21 HTML / CSS
html5 application cache遇到的严重问题
2012/12/26 HTML / CSS
html5 外链式实现加减乘除的代码
2019/09/04 HTML / CSS
萌新的HTML5 入门指南
2020/11/06 HTML / CSS
澳大利亚最受欢迎的美发和美容在线商店:Catwalk
2018/12/12 全球购物
法国隐形眼镜网站:VisionDirect.fr
2020/03/03 全球购物
艺术设计专业个人求职信
2014/04/10 职场文书
北京英文导游词
2015/02/12 职场文书
2015年高三班主任工作总结
2015/05/21 职场文书
2016春节放假通知范文
2015/08/18 职场文书
2016教师给学生的毕业寄语
2015/12/04 职场文书