JavaScipt 事件体系

Posted on 2017-01-05 18:30  黄银  阅读(328)  评论(0编辑  收藏  举报

事件机制

jQuery对事件的绑定分别有几个API

.bind()/.live()/.delegate()/.on()

不管是用什么方式绑定,归根到底还是用addEventListener/attachEvent处理的,正如选择器一样不管如何匹配最终还是那么几个浏览器接口处理,既然如此,事件为什么还要区分那么多不同的处理方案?

这里就要涉及到 DOM 事件处理模型了,就是常提到的捕获与冒泡传统的事件处理给某一元素绑定了一个点击事件,传入一个回调句柄处理。

element.addEventListener('click',doSomething,false)

这样的代码再正常不过了,但是,如果页面上有几百个元素需要绑定(假设),那么务必就要绑定几百次啦。
这样问题就出现了:

第一:大量的事件绑定,性能消耗,而且还需要解绑(IE会泄漏)
第二:绑定的元素必须要存在
第三: 后期生成HTML会没有事件绑定,需要重新绑定
第四: 语法过于繁杂

有没有办法优化呢?答案是肯定的,那就是采用委托的机制

事件委托

DOM 有个事件流的特性,也就是说我们在页面上触发节点的时候事件都会上下或者向上传播,事件捕捉和事件冒泡。

DOM2.0 模型将事件处理流程分为三个阶段:

一、事件捕获阶段
二、事件目标阶段
三、事件起泡阶段


事件传送可以分为3个阶段。
(1)在事件捕捉(Capturing)阶段,事件将沿着DOM树向下转送,目标节点的每一个祖先节点,直至目标节点。例如,若用户单击了一个超链接,则该单击事件将从document节点转送到html元素,body元素以及包含该链接的p元素。在此过程中,浏览器都会检测针对该事件的捕捉事件监听器,并且运行这件事件监听器。
(2)在目标(target)阶段,浏览器在查找到已经指定给目标事件的事件监听器之后,就会运行该事件监听器。目标节点就是触发事件的 DOM 节点。例如,如果用户单击一个超链接,那么该链接就是目标节点(此时的目标节点实际上是超链接内的文本节点)。
(3)在冒泡(Bubbling)阶段,事件将沿着DOM树向上转送,再次逐个访问目标元素的祖先节点到document节点。该过程中的每一步。浏览器都将检测那些不是捕捉事件监听器的事件监听器,并执行它们。
利用事件传播(这里是冒泡)这个机制,就可以实现事件委托

具体来说,事件委托就是事件目标自身不处理事件,而是把处理任务委托给其父元素或者祖先元素,甚至根元素(document)

委托这么好的特性 jQuery 当然不会放过,所以就衍生出  .bind()、.live() .on()和.delegate(),jQuery 的事件绑定有多个方法可以调用,以 click 事件来举例:

click方法
bind方法
delegate方法
on方法

这里要清楚的认识:不管你用的是(click / bind / delegate)之中哪个方法,最终都是 jQuery 底层都是调用 on 方法来完成最终的事件绑定。因此从某种角度来讲除了在书写的方便程度及习惯上挑选,不如直接都采用 on 方法来的痛快和直接。

所以在新版的 API 中都这么写到:

.on()方法事件处理程序到当前选定的 jQuery 对象中的元素。在jQuery 1.7中,.on()方法提供绑定事件处理的所有功能、效果不言而喻了,除了性能的差异,通过委托的事件还能很友好的支持动态绑定,只要 on 的delegate 象是 HTML 页面原有的元素,由于是事件的触发是通过Javascript的事件冒泡机制来监测,所以对于所有子元素(包括后期通过JS生成的元素)所有的事件监测均能有效,且由于不用对多个元素进行事件绑定,能够有效的节省内存的损耗。

几种绑定

采用事件委托最直观的感受就是,不需要给每一个指定的元素绑定事件,从而降低了繁琐的绑定过程,节约了代码量,同时也节约了内存的开销。绑定一个事件都是需要占用内存消耗的,除了性能的差异,通过委托的事件还能很友好的支持动态绑定,只要 on 的 delegate 对象是 HTML 页面原有的元素,由于是事件的触发是通过 Javascript 的事件冒泡机制来监测,所以对于所有子元素(包括后期通过 JS 生成的元素)所有的事件监测均能有效,且由于不用对多个元素进行事件绑定,能够有效的节省内存的损耗。

那么 jQuery 对事件的绑定分别有几种 API,具体有什么区别我们来了解一下:

bind方法

.bind()方法用于直接附加一个事件处理程序到元素上,处理程序附加到 jQuery 对象中当前选中的元素,所以在 .bind() 绑定事件的时候这些元素必须已经存在,很明显就是直接调用没利用委托机制。

live方法

将委托的事件处理程序附加到一个页面的 document 元素,从而简化了在页面上动态添加的内容上事件处理的使用。

例如:

$('a').live('click', function() { alert("!")})

JQuery 把 alert 函数绑定到 $(document) 元素上,并使用 ’click’和 ’a’作为参数。任何时候只要有事件冒泡到 document 节点上,它就查看该事件是否是一个 click 事件,以及该事件的目标元素与’a’这一CSS 选择器是否匹配,如果都是的话,则执行函数。

因为更高版本的 jQuery 提供了更好的方法,没有 .live() 方法的缺点,所以 .live() 方法不再推荐使用,特别是使用 .live() 出现的以下问题:

  1. 在调用 .live() 方法之前,jQuery 会先获取与指定的选择器匹配的元素,这一点对于大型文档来说是很花费时间的。
  2. 不支持链式写法。例如,$("a").find(".offsite, .external").live( ... ); 这样的写法是不合法的,并不能像期待的那样起作用。
  3. 由于所有的 .live() 事件被添加到 document 元素上,所以在事件被处理之前,可能会通过最长最慢的那条路径之后才能被触发。
  4. 在移动 iOS (iPhone, iPad 和 iPod Touch) 上,对于大多数元素而言,click 事件不会冒泡到文档 body 上,并且如果不满足如下情况之一,就不能和 .live() 方法一起使用:使用原生的可被点击的元素,例如, a 或 button,因为这两个元素可以冒泡到 document。
  5. 在 document.body 内的元素使用 .on() 或 .delegate() 进行绑定,因为移动 iOS 只有在 body 内才能进行冒泡。
  6. 需要 click 冒泡到元素上才能应用的 CSS 样式 cursor:pointer (或者是父元素包含document.documentElement)。但是依然需要注意的是,这样会禁止元素上的复制/粘贴功能,并且当点击元素时,会导致该元素被高亮显示。
  7. 在事件处理中调用 event.stopPropagation() 来阻止事件处理被添加到 document 之后的节点中,是效率很低的,因为事件已经被传播到 document 上。
  8. .live() 方法与其它事件方法的相互影响是会令人感到惊讶的。例如,$(document).unbind("click") 会移除所有通过 .live() 添加的 click 事件!
     

delegate方法

为了突破单一 .bind() 方法的局限性,实现事件委托,jQuery 1.3引入了.live()方法。后来,为解决“事件传播链”过长的问题,jQuery 1.4又支持为 .live() 方法指定上下文对象。而为了解决无谓生成元素集合的问题,jQuery 1.4.2干脆直接引入了一个新方法 .delegate()

使用 .delegate(),前面的例子可以这样写:

$('#element).delegate('a', 'click', function() { alert("!!!") });

jQuery 扫描文档查找(‘#element’),并使用 click 事件和’a’这一CSS选择器作为参数把 alert 函数绑定到(‘#element)上。

任何时候只要有事件冒泡到$(‘#element)上,它就查看该事件是否是click事件,以及该事件的目标元素是否与CCS选择器相匹配。如果两种检查的结果都为真的话,它就执行函数。

可以注意到,这一过程与.live()类似,但是其把处理程序绑定到具体的元素而非document这一根上。

那么 (′a′).live()==(document).delegate('a') ?

可见,.delegate() 方法是一个相对完美的解决方案。但在DOM结构简单的情况下,也可以使用.live()。

on方法

其实 .bind(), .live(), .delegate()都是通过.on()来实现的,.unbind(), .die(), .undelegate()也是一样的都是通过.off()来实现的,提供了一种统一绑定事件的方法。

总结:

在下列情况下,应该使用 .live()或 .delegate(),而不能使用 .bind():

1. 为DOM中的很多元素绑定相同事件;
2. 为DOM中尚不存在的元素绑定事件;
3. 用.bind()的代价是非常大的,它会把相同的一个事件处理程序hook到所有匹配的DOM元素上
4. 不要再用.live()了,它已经不再被推荐了,而且还有许多问题
5. .delegate()会提供很好的方法来提高效率,同时我们可以添加一事件处理方法到动态添加的元素上

我们可以用 .on() 来代替上述的 3 种方法。

不足点也是有的:

1. 并非所有的事件都能冒泡,如load, change, submit, focus, blur
2. 加大管理复杂
3. 不好模拟用户触发事件
4. 如何取舍就看项目实际中运用了

事件接口

jQuery事件处理机制能帮我们处理那些问题?

毋容置疑首先要解决浏览器事件兼容问题:

1. 可以在一个事件类型上添加多个事件处理函数,可以一次添加多个事件类型的事件处理函数
2. 提供了常用事件的便捷方法
3. 支持自定义事件
4. 扩展了组合事件
5. 提供了统一的事件封装、绑定、执行、销毁机制

……

为了更深入的理解幕后的实现,所以先整理整体的结构思路,从1.7后就去除了 live 绑定,所以现在的整个事件的 API

如图:

jQuery的事件绑定有多个方法可以调用,以 click 事件来举例:

$('#foo').click(function(){ })
$('#foo').bind('click',function(){ })
$("foo").delegate("td", "click", function() { })
$("foo").on("click", "td", function() { })

click,bind,delegate,on方法,以上四种绑定都能达到同一样的效果,但是各自又有什么区别,内部又是如何实现?

源码分析

click方式

jQuery.fn[ 'click' ] = function( data, fn ) {
    return arguments.length > 0 ?
         this.on( name, null, data, fn ) :
         this.trigger( name );
};

源码很简单,合并15种事件统一增加到jQuery.fn上,内部调用this.on / this.trigger。

bind方式

bind: function( types, data, fn ) {
    return this.on( types, null, data, fn )
}

同样调用的this.on/this.off。

delegate方式

delegate: function( selector, types, data, fn ) {
    return this.on( types, selector, data, fn )
}

同样调用的this.on/this.off。

one方式

one: function( types, selector, data, fn ) {
    return this.on( types, selector, data, fn, 1 )
}

可见以上的接口只是修改了不同的传递参数,最后都交给 on 实现的。

体系结构

我们看看 jQuery 在针对事件的整个处理的过程,包括绑定执行到底都干了些什么,通过一个流程图我们大体的了解下。

jQuery事件的流程图,在绑定阶段与执行阶段:

(单击图片可放大)

以上是 jQuery 事件的整个结构流程图,右边是流程的一个简单实现,主要是用于理解,源码当然不是这么简单的,考虑代码量太多,有些机制是没有实现的,比如委托与原生事件的区分:

1. 通过 on 绑定事件,分析传递的数据,加工变成 add 能够识别的数据
2. 通过 add 把数据整理放到数据缓存中保存,通过 addEventListener 绑定事件
3. 触发事件执行 addEventListener 回调 dispatch 方法
4. 修正事件对象存在的问题,通过 fix 生成一个可写的事件对象
5. 引入 handlers 把委托和原生事件(例如"click")绑定区分对待
6. 执行数据缓存的事件回调,传入内部产生的事件对象

这样的好处:

  1. 因为事件对象是我们重写生成的,所以我们可以内部获取到状态值,比如是否冒泡,是否阻止默认行为。
  2. 通过事件的委托机制我们可以让原本不支持冒泡的元素也同样模拟出来,比如 blur,focus,具体怎么实现我们后面再讲解。

 

绑定设计

.on(events[,selector][,data],handler(eventObject))

1,events:事件名

2,selector:一个选择器字符串,用户过滤出被选中的元素中能触发的后代元素

3,data:当一个事件被触发时,要传递给事件处理函数的

4,handler:事件被触发时,执行的函数

用来绑定一个事件

var body = $("body")

body.on("click",'p',function(){

  console.log(this)

});

用on方法给body上绑定一个click事件,冒泡到p元素的时候才触发回调函数,每次在body上点击其实都会触发事件,但是只目标为p元素的情况下才会触发回调的处理函数,通过源码会发现实质只完成一些参数调整的工作,而实际负责事件绑定的是内部jQuery.event.add方法。

绑定的实际接口on的代码

on:function(types,selector,data,fn){

  return this,each(function(){

    jQuery.event.add(this,types,fn,data,selctor);

  });

}

jQuery.event.add内部实际上最终还是id铜鼓odaddEventListener绑定的事件

其中一些变量代码的意思

1,elem:目标元素

2,type:事件类型,click

3,eventHandle:事件句柄,也就是事件回调处理的内容了

4,false:冒泡

5.elem:目标元素type:事件类型,如click:eventHandle事件句柄,也就是事件回调处理的内容 false:冒泡

现在我们把之前的案例给套一下看看:

var body = document.getElementsByTagName('body')
var eventHandle = function() {
    console.log(this)
}
body.addEventListener('click', eventHandle, false);

如果是我们自己实现的这个代码是有问题的,我们在body上每次都触发了click事件,但是我们并没有委托的p元素的处理,自然也达不到委托的效果。

eventHandle源码

回到内部绑定的事件句柄 eventHandle ,可想而知 eventHandle 不仅仅只是只是充当一个回调函数的角色,而是一个实现了 EventListener 接口的对象。

if (!(eventHandle = elemData.handle)) {
    eventHandle = elemData.handle = function(e) {
        return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ?
            jQuery.event.dispatch.apply(elem, arguments) : undefined;
    };
}

可见在 eventHandle 中并没有直接处理回调函数,而是映射到 jQuery.event.dispatch 分派事件处理函数了仅仅只是传入 eventHandle.elem,arguments , 就是 body 元素 与事件对象那么这里有个问题,事件回调的句柄并没有传递过去,后面的代码如何关联?本章的一些地方可能要结合后面的 dispatch 处理才能清理,但是我们还是先看看做了那些处理。

一个简单的流程图:

 同一个元素绑定多个相同或者不同的事件要如何处理?

首先我们脑海中先要有一个分离的概念,jQuery 的事件与数据其实是没有直接关联的关系,而且通过数据缓存去存储事件数据的。jQuery 从1.2.3版本引入数据缓存系统贯穿内部,为整个服务事件体系也引入了这个缓存机制,所以 jQuery 并没有将事件处理函数直接绑定到 DOM 元素上,而是通过 .data 存储在缓存 .cahce 上,所以事件的机制都是依赖之前的数据缓存模块的。

我们为了理解 jQuery.event.add 代码结构,适当的跳过这个环节中不能理解的代码。

1,获取数据缓存

elemData = data_priv.get(elem);

通过缓存对象的get方法获取对应的存储数据,如果没有则内部会新建一个与elem元素映射的数据缓存区,用来存储之后用户将要处理操作行为(事件与事件),这个处理主要是合并同个元素绑定多个事件的问题。

arron.on("mousedown",'li',function(e){show("1")});

arron.on("mousedown",'ul',function(e){show("2")});

arron.on('mousedown','div',function(e){show("3")});

arron.on("mousedown",function(e){show('4')});

如上同一个元素上绑定了 4 次不同的行为,但是都是针对同一个元素的所以这个地方我们就需要把事件与数据都合并到同一个缓存区,这样每次重复操作都不会在去创建一个新的缓存了。

 

 第二步 创建编号

if(!hander.guid){

  handler.guid = jQuery.guid++;

}

 

 为每一个事件的句柄给一个标示,添加 ID 的目的是用来寻找或者删除 handler,因为这个东东是缓存在缓存对象上的,没有直接跟元素节点发生关联。

 

第三步 给缓存增加事件处理句柄

if ( !(events = elemData.events) ) {
    events = elemData.events= {};
}
if ( !(eventHandle = elemData.handle) ) {
    eventHandle = elemData.handle = function( e ) {
        return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ?
            jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
            undefined;
    };
}
events,eventsHandle都是elemData缓存对象内部的,可以在elemData中有两个重要的属性
1,一个是events,是jQuery内部维护的时间队列
2,一个是handle,是实际绑定到elem中事件处理函数

第四步 填充事件名与事件句柄
事件名称可以添加指定的event.namePlugin.simple为click事件同时定义了两个命名空间myPligin和silple,通过上述方法绑定的click事件处理,可以用off("click.myPlugin")或off("click.simple")删除绑定到到相应元素的 Click 事件处理程序,而不会干扰其他绑定在该元素上的“click(点击)” 事件。命名空间类似 CSS 类,因为它们是不分层次的;只需要有一个名字相匹配即可。以下划线开头的名字空间是供 jQuery 使用的。所以要针对每个事件都需要绑定。

如:

.on('mouseup mousedown','p',function(e){
    console.log(e)
  })

其实就是填充events与eventHandle

elemData = {
       events:{}
       eventHandle:function(){}
}
handlers = events[type] = [];
handlers.delegateCount = 0;

这段比较长了分解下,最终的目的就是为填充events,eventHandle。

其实整个 add 方法下来就干了那么几件事:

  1. 利用 data_priv 数据缓存,分离事件与数据
  2. 元素与缓存中建立 guid 的映射关系用于查找
  3. 通过 elemData.events 合并同一个元素上的多个事件
  4. 通过空格分隔的多事件
  5. 引用了钩子处理特殊事件
  6. 如果委托元素,给对应的数据打上一个记录标记
  7. 最后通过 addEventListener 绑定事件,等待执行

 

Special Event机制

在事件处理中,很多地方都用到了这个 jQuery.event.special 方法,special 是一个在处理特殊的事件相当灵活,可以指定绑定和解开钩子以及定制事件的默认行为,用这个 API 的时候可以创建自定义的事件但不仅仅是执行绑定事件处理程序时,引发这些“特殊”事件可以修改事件对象传递给事件处理程序,引发其他完全不同的事件,或者执行复杂的 setup 和 teardown 代码当事件处理程序绑定到或未绑定元素。

某些事件类型的有特殊行为和属性,换句话说就是某些事件不是大众化的事件不能一概处理。

比如 load 事件拥有特殊的 noBubble 属性,可以防止该事件的冒泡而引发一些错误,所以需要单独针的处理,但是如果都写成判断的形式,显然代码结构就不合理了,而且不方便提供给用户自定义扩展。有些浏览器并不兼容某类型的事件,如 IE6~8 不支持 hashchange 事件,你无法通过 jQuery(window).bind('hashchange', callback) 来绑定这个事件,这个时候你就可以通过 jQuery 自定义事件接口来模拟这个事件,做到跨浏览器兼容。这个就是 special 的作用了,又有点类似之前说的钩子机制了。

原理

jQuery(elem).bind(type, callbakc) 实际上是映射到 jQuery.event.add(elem, types, handler, data) 这个方法,每一个类型的事件会初始化一次事件处理器,而传入的回调函数会以数组的方式缓存起来,当事件触发的时候,处理器将依次执行这个数组。

jQuery.event.add 方法在第一次初始化处理器的时候,会检查是否为自定义事件,如果存在则将会把控制权限交给自定义事件的事件初始化函数,同样事件卸载的 jQuery.event.remove 方法在删除处理器前也会检查此处。

初始化处事件处理器

if (!special.setup || special.setup.call(elem, data, namespaces, eventHandle) === false) {
   if (elem.addEventListener) {
      elem.addEventListener(type, eventHandle, false);
   }
}

卸载自定义事件

if (!special.teardown || special.teardown.call(elem, namespaces, elemData.handle) === false) {
    jQuery.removeEvent(elem, type, elemData.handle);
}

jQuery.event.special 对象中,保存着为适配特定事件所需的变量和方法。

beforeunload: Object
blur: Object
click: Object
focus: Object
focusin: Object
focusout: Object
load: Object
mouseenter: Object
mouseleave: Object
pointerenter: Object
pointerleave: Object

事实上 jQuery 自定义事件那些接收的参数有点鸡肋,需要 hack 与能 hack 的事件就那么一点点,且限制颇多,一般情况下很少使用到。

得出总结:

在 jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :

方法中没有传递回调对象是因为回调的句柄被关联到了 elemData,也就是内部数据缓存中了,不难得出 jQuery 的事件绑定机制:jQuery 对每一个 elem 中的每一种事件,只会绑定一次事件处理函数(绑定这个elemData.handle),而这个 elemData.handle 实际只做一件事,就是把 event 丢到 jQuery 内部的事件分发程序。

jQuery.event.dispatch.apply( eventHandle.elem, arguments );

而不同的事件绑定,具体是由 jQuery 内部维护的事件列队来区分(就是那个 elemData.events),在 elemData 中获取到 events 和 handle 之后,接下来就需要知道这次绑定的是什么事件了。

 

理解委托

在绑定中我们知道,事件信息都存储在数据缓存中,对于没有特殊事件特有监听方法和普通事件都用addEventListener来添加事件了,而特有监听方法的特殊事件则用了另一种方式来添加事件,通过addEventListener触发事件后回调句柄如何处理?

 

 默认的触发循序是从事件源目标也就是event.target指定的元素,一直往上冒泡到document或者body,途径的元素上如果对应的事件都会被依次触发。

最后得到的结论

元素本身绑定事件的顺序处理机制,分几种情况

1,A,B,C各自绑定事件,事件按照节点的冒泡层次触发

2,元素a本身有事件,元素还需要委派元素,b,c事件委派的元素,bc肯定是元素a内部的,所以先处理内部的委派,最后处理本身的事件。

3,元素本身有事件,元素还需要委派事件,内部委派的元素还有自己的事件,这个有点绕,先执行bc自己本身的事件,然后处理bc委派的事件,最后处理a事件。

 

事件对象

既然可以冒泡,响应的也应该可以停止,这里需要利用到事件的特有对象,事件对象,是JavaScript中一个非常重要的对象,用来表示当前事件,event对象的属性和方法包含了当前事件的状态

当前事件是指正在发生的事件,是与事件有关的性质,如引发事件的DOM元素,鼠标的状态,按下的键等等。

event对下你给只在事件发生的过程中才有效,事件对象提供了preventDefault,stopPropagation2个方法一个停止事件传播,一个传递默认的行为(暂且无视ie),jQuery提供了个万能的return false不仅可以阻止事件冒泡,还可以阻止浏览器的默认行为,还可以减少ie系列的bug,其实就是根据返回的布尔值调用preventDefault,stopPropagation方法。e.stoplmmediatePropagationd方法不仅阻止了一个事件的冒泡的,也把这个元素上的其他绑定事件也阻止了。

浏览器的实现差异

在 W3C 规范中,event 对象是随事件处理函数传入的,Chrome、FireFox、Opera、Safari、IE9.0及其以上版本都支持这种方式。但是对于 IE8.0 及其以下版本,event 对象必须作为 window 对象的一个属性。
在遵循 W3C 规范的浏览器中,event 对象通过事件处理函数的参数传入,event 的某些属性只对特定的事件有意义。比如,fromElement 和 toElement 属性只对 onmouseover 和 onmouseout 事件有意义。
特别指出:分析的版本是2.1.1,已经不再兼容 IE6-7-8了,所以部分兼容问题都已经统一,例如:事件绑定的接口,事件对象的获取等等,我们源码分析的重点不在使用,而是设计的思路。
注:事件对象具体有些什么方法属性参照 http://www.itxueyuan.org/view/6340.html。

事件对象中我们用的最多的就是 target了,这个是我们的点击对象,别忘记了还有个 currentTarget 这个是事件的绑定对象,有什么区别?

<div id="aaron">
   <div>
     <p>Click me!</p>
   </div>
</div>
var aaron = document.getElementById('aaron')
aaron.addEventListener('click',function(e){
    console.log(this,this == e.currentTarget,e)
},false)

如上结构,currentTarget 是 aaron 的 div 元素 , target 是 p 元素,事件对象是有作用域的 currentTarget 是等于 this 的。
事件对象的基础大家都是知道了,jQuery为了实现统一的事件对象调用与委托的的处理,将事件对象单独重写,这样如果用户做了任何的行为处理 jQuery 内部都能获取到状态值,从而用来处理同一个元素绑定多个模拟事件的判断处理。这也是重写后的一个重要意义。

重写事件对象

jQuery 为 dom 处理而生,那么处理兼容的手段自然是独树一帜了,所以:

jQuery对事件的对象的兼容问题单独抽象出一个 fix 类,用来重写这个事件对象

jQuery 利用 jQuery.event.fix() 来解决跨浏览器的兼容性问题,统一接口。

除该核心方法外,我们要根据事件的类型,统一接口的获取,所以 jQuery 引入了 (jQuery.event) props、 fixHooks、keyHooks、mouseHooks 等数据模块。

  1. props 存储了原生事件对象 event 的通用属性
  2. keyHook.props 存储键盘事件的特有属性
  3. mouseHooks.props 存储鼠标事件的特有属性。
  4. keyHooks.filter 和 mouseHooks.filter 两个方法分别用于修改键盘和鼠标事件的属性兼容性问题,用于统一接口。
  5. 比如 event.which 通过 event.charCode 或 event.keyCode 或 event.button 来标准化。

最后 fixHooks 对象用于缓存不同事件所属的事件类别,比如:

fixHooks['click'] === jQuery.event.mouseHooks;
fixHooks['keydown'] === jQuery.event.keyHooks;
fixHooks['focusin'] === {};

从源码处获取对事件对象的操作,通过调用 jQuery.Event 重写事件对象,将浏览器原生 Event 的属性赋值到新创建的 jQuery.Event 对象中去。

event = new jQuery.Event( originalEvent );

event 就是对原生事件对象的一个重写了,为什么要这样,jQuery要增加自己的处理机制呗,这样更灵活,而且还可以传递 data 数据,也就是用户自定义的数据。

构造出来的新对象:

 

看图,通过 jQuery.Event 构造器,仅仅只有一些定义的属性与方法,但是原生的事件对象的属性是不是丢了?

所以还需要把原生的的属性给混入到这个新对象上,那么此时带来一个问题,不同事件会产生了不同的事件对象,拥有不同的属性,所以还有一套适配的机制,根据不同的触发点去适配需要混入的属性名。

扩展通过 jQuery.Event 构造出的新事件对象属性

//扩展事件属性
this.fixHooks[ type ] = fixHook =
    rmouseEvent.test( type ) ? this.mouseHooks :
        rkeyEvent.test( type ) ? this.keyHooks :
        {};

有一些属性是共用的,都存在,所以单独拿出来就好了。

props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),

然后把私有的与公共的拼接一下。

copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;

然后混入到这个新的对象上。

jQuery 自己写了一个基于 native event 的 Event 对象,并且把 copy 数组中对应的属性从 native event 中复制到自己的 Event 对象中。

while ( i-- ) {
    prop = copy[ i ];
    event[ prop ] = originalEvent[ prop ];
}

在最后 jQuery 还不忘放一个钩子,调用 fixHook.fitler 方法用以纠正一些特定的 event 属性。例如 mouse event 中的 pageX,pageY,keyboard event中的 which,进一步修正事件对象属性的兼容问题。

fixHook.filter? fixHook.filter( event, originalEvent ) : event

fixHook 就是在上一章,预处理的时候用到的,分解 type 存进去的,针对这个特性的单独处理,最后返回这个“全新的”Event 对象。

总的来说 jQuery.event.fix 做的事情:

1.将原生的事件对象 event 修正为一个新的可写 event 对象,并对该 event 的属性以及方法统一接口
2.该方法在内部调用了 jQuery.Event(event) 构造函数

 

 

委托实现

在执行事件的时候,jQuery 会根据事件绑定的时候处理来执行事件的逐渐触发,我们观察一组代码:

div.on('mousedown', 'li', function(e) {
  show('委托到li触发')
})
div.on('mousedown', 'ul', function(e) {
  show('委托到ul触发')
})
div.on('mousedown', 'a', function(e) {
  show('委托到a触发')
})
div.on('mousedown', function(e) {
  show('mousedown')
})

给 div 元素绑定一个 mousedown 事件,但是在 div 元素上触发的时候,其实之前还会先触发 li、ul、a,3 个元素的事件,这是因为事件都是绑定的四个事件,3 个是通过委托到 div 元素上触发的,那么这个委托是如何处理的?

设计的思路解析:

委托的实现说起来很简单,我们事件对象里面不是有一个 target 属性吗? target 就指的触发的目标对象。

target 的理解:

<ul>
 <li>点击执行委托链</li>
</ul>
  1. 给 ul 绑定一个事件,那么点击 li 的时候 li 就是目标对象
  2. 如果给 li 绑定一个事件,点击 li 目标对象就是自身了
通过 target 与实际的事件绑定对象我们就可以划分一个区域段,通过递归获取每一个元素的 parentNode 节点,在每一个节点层上通过与委托节点的对比用来确定是不是委托的事件元素,这个就是委托的核心思路了

这个处理 jQuery 交给了 $.event.handlers 方法,就上一个结构我们 handlers 这样分解

  1. 点击触发事件,触发任意一个 target 元素
  2. 通过递归回溯 target.parentNode 来找到 li 元素或者 ul 元素,当然递归的最终直到 div 节点
  3. 如果在任意个递归中找到了委托的节点那么就这个元素给保存起来,这里暂时不处理,这样我们先确定好这个触发的队列数据
简单来说就是把 target 到根节点 div 通过 node.parentNode 遍历一遍,然后找到对应的委托元素节点,如果符合就缓存起来用于之后的操作,可以通过 jQuery.event.handlers 方法我们可以获取类似这种的一组数据结构

那么过滤之后的结构就是这样了:通过 handlerQueue 保存需要的委托队列数据


从这里我们可以看出 delegate 绑定的事件和普通绑定的事件是如何分开的,对应一个元素一个 event.type 的事件,处理对象队列在缓存里只有一个,按照冒泡的执行顺序与元素的从内向外递归以及 handlers 的排序,所以就处理了,就形成了事件队列的委托在前,自身事件在后的顺序,这样也跟浏览器事件执行的顺序一致了。

区分delegate绑定和普通绑定的方法是:delegate 绑定从队列头部推入,而普通绑定从尾部推入,通过记录 delegateCount 来划分,delegate 绑定和普通绑定

我们按照委托的顺序遍历这个结构

while ((matched = handlerQueue[i++]) && !event.isPropagationStopped()) {
  event.currentTarget = matched.elem;
  j = 0;
  while ((handleObj = matched.handlers[j++]) && !event.isImmediatePropagationStopped()) {
      ret = handleObj.handler.apply(matched.elem, args);
      //如果返回了false
      if (ret !== undefined) {
        if ((event.result = ret) === false) {
          event.preventDefault();
          event.stopPropagation();
}

因为结构上来说,同一个 div 上绑定多个委托元素,那么事件对象引用就是同样的,event.isPropagationStopped 引用永远是 div 的事件对象 div,ul与 p 都是共用的,只是在不同的元素上面修改delegateTarget 与 currentTarget,所以在之前重写的事件对象就发挥作用了,如果在一个元素上调用了stopPropagation 那么后面的事件自然都不会触发了,因为 event.isPropagationStopped 会获取这个状态
总的来说 jQuery.event.handlers 做的事情:

    1. 将有序地返回当前事件所需执行的所有事件处理程序。
    2. 这里的事件处理程序既包括直接绑定在该元素上的事件处理程序,也包括利用冒泡机制委托在该元素的事件处理程序(委托机制依赖于 selector)。
    3. 在返回这些事件处理程序时,委托的事件处理程序相对于直接绑定的事件处理程序在队列的更前,委托层次越深,该事件处理程序则越靠前。

 

 自定义事件

在js中,消息的通知是通过事件表达的,当代码库增长到一定的规模就考虑将行为进行解耦, 通过事件机制可以将类设计独立的模块,通过事件对外通信提高了程序的开发效率。

自定义事件的概念:

1.类似DOM的行为:在DOM节点(包括document对象)监听并触发自定义事件,这些事件既可以冒泡,也可以被拦住,这正是Prototype,jQuery和MooTools所做的,如果事件不能扩散,就必须在触发事件的对象上进行监听。

2,命名空间:一些框架需要你为事件指定命名空间,通常使用一个点号前缀来把你的事件和原生事件区分开。

3,自定义额外数据:JavaScript框架允许你在触发自定义事件时,向事件处理器传送额外的数据,jQuery可以向数据处理器传递任意数据的额外参数。

4,通用事件API:只用Dojo保留了操作原生DOM事件的正常API,而操作指定事件需要特殊的发布、订阅api,这意味着这Dojo中自定义事件不具有DOM事件的一些行为(比如冒泡)。

5.我们往往需要在预定义的事件中加入一些特殊的变化(例如,需要Alt键按下才能触发的单击事件),MooTools 运行你定义此类自定义事件。此类事件需要预先声明,即便你只是声明他们的名字。任何未声明的自定义事件不会被触发。

jQuery 的事件自定义事件还是通过 on 绑定的,然后再通过 trigger 来触发这个事件。

//给element绑定hello事件
element.bind("hello",function(){
    alert("hello world!");
});
//触发hello事件
element.trigger("hello");

这段代码这样写似乎感觉不出它的好处,看了下面的例子也许你会明白使用自定义事件的好处了,参考右边的代码。


trigger需要处理的问题

  1. 模拟事件对象,用户模拟处理停止事件冒泡(因为不是通过浏览器系统触发的,而是自动触发的,所以这个事件对象要如何处理?)
  2. 区分事件类型,触发标准的浏览器事件 和 自定义事件名绑定的处理程序。

拟冒泡机制

    1. 当事件是 click 类型,自然是本身支持冒泡这样的行为,通过 stopPropagation 阻止即可
    2. 当然一些事件,如 focusin 和 blur 本身不冒泡,但 jQuery 为了跨浏览器一致性, jQuery 需要在这些事件上模拟了冒泡行为,jQuery 要如何处理?
    3. 那么如果是自定义的aaa的事件名,又如何处理冒泡?
事件trigger
针对自定义事件jQuery有一个trgger方法,代码其实不是很复杂,但是由于关联性太强了所以非常绕。
  
通过triangle手动触发了foo元素的click事件,body的click事件
$("body").click(function(event,data){
  console.log(1);
});
var foo = $("#foo");
foo.on("click",function(event,data){
  console.log(data);
});
foo.trigger("click","慕课网");
 
元素foo本身绑定了一个click事件,但是我们知道click这种原生事件是靠addEventListener绑定交互驱动的,但是jQuery的trigger能够在任意时刻模拟这个交互行为。
从这一个功能点上我们就不难发现为什么jQuery要设计元素与元素分离了,如果是直接绑定的话就完全无法通过trigger的机制调用,trigger的实现首先得有益于事件的分离机制,因为没有直接把事件相关的与元素直接绑定采用了分离处理,所以我们通过trigger触发与addEventListener触发的处理流程都是一致的,不同的只是触发的方式而已,通过trigger触发的事件是没有事件对象,冒泡这些特性的,所以我们需要一个方法能模拟出事件对象,然后生成一个遍历模拟出冒泡行为,那么这个任务就交给trigger方法了。
 
trigger与dispatch方法的区别
1,jquery的事件我们应用从更抽象的一层去理解它的元素层次划分其实是非常清晰的,首先每一个元素都可以绑定事件与冒泡,那么这个针对每个层的单独元素的处理是划分给了dispatch方法,在dispatch方法中我们通过target与在 dispatch 方法中我们通过 target 与 currentTarget(绑定事件的元素)生成一条冒泡线,依次往父层元素遍历取出每一个层级元素对应的数据相应的执行,由于在这个模拟冒泡的操作过程中,jQuery 模拟出的事件对应被所有的这些操作共享,所以在任何一个元素的事件处理中调用了停止冒泡,那么这个循环就停止了,也就达到了 stopPropagation 的目的,这里我们要注意事件的冒泡是在绑定事件元素内部发生的
2.原生事件提供了一个最重要参数 - 事件对象,trigger 是模拟触发,所以我们需要模拟一个这样的数据对象,其次 trigger 也要支持冒泡,但是这里有一个区别 dispatch 的地方,trigger 冒泡的 target 的对象是确定的,所以 target 就是自己本身,所以冒泡的路径其实是一个从自己本身到 window 的一条外部路线。

jQuery 设计好的地方就是对元素层级的划分,内部冒泡与外部冒泡独立处理,相互不会影响,但是又有千丝万缕的关系,具体我们来看看处理的结构。

初看 trigger 源码部分真有点晕,处理的 hack 太多了:

  1. 命名空间的过滤
  2. 模拟事件对象
  3. 制作一个触发的路径队列eventPath
  4. 对 eventPath 进行模拟冒泡的触发
  5. 在一个层级调用 dispatch 处理各自的内部事件关系(委托)

总结

所以整个 trigger 的核心,还是围绕着数据缓存在处理的,通过 on 机制在 jQuery.event.add 的时候预处理好了。trigger 的处理就是模拟冒泡的一个调度,具体的触发还是交给 jQuery.event.dispatch 方法了,通过 trigger 很好的模拟了浏览器事件流程,但是美中不足的是对象的事件混淆其中,这就造成了触发对象事件的时候最后会调用对象的相应方法。