JavaScript事件代理入门

JS - 事件代理

      如果你想给网页添加点JavaScript的交互性,也许你已经听过JavaScript的事件代理(event delegation),并且觉得这是那些发烧友级别的JavaScript程序员才会关心的什么费解的设计模式之一。事实上,如果你已经知道怎么添加JavaScript的事件处理器(event handler),实现事件代理也是件轻而易举的事情。

     JavaScript事件是所有网页互动性的根基(我指的是真正的互动性,而不仅是那些CSS下拉菜单)。在传统的事件处理中,你按照需要为每一个元素添加或者是删除事件处理器。然而,事件处理器将有可能导致内存泄露或者是性能下降——你用得越多这种风险就越大。JavaScript事件代理则是一种简单的技巧,通过它你可以把事件处理器添加到一个父级元素上,这样就避免了把事件处理器添加到多个子级元素上。

它是怎么运作的呢?

      事件代理用到了两个在JavaSciprt事件中常被忽略的特性:事件冒泡以及目标元素。当一个元素上的事件被触发的时候,比如说鼠标点击了一个按钮,同样的事件将会在那个元素的所有祖先元素中被触发。这一过程被称为事件冒泡;这个事件从原始元素开始一直冒泡到DOM树的最上层。任何一个事件的目标元素都是最开始的那个元素,在我们的这个例子中也就是按钮,并且它在我们的元素对象中以属性的形式出现。使用事件代理,我们可以把事件处理器添加到一个元素上,等待一个事件从它的子级元素里冒泡上来,并且可以得知这个事件是从哪个元素开始的。

这对我有什么好处呢?

      想象一下现在我们有一个10列、100行的HTML表格,你希望在用户点击表格中的某一单元格的时候做点什么。比如说我有一次就需要让表格中的每一个单元格在被点击的时候变成可编辑状态。如果把事件处理器加到这1000个单元格会产生一个很大的性能问题,并且有可能导致内存泄露甚至是浏览器的崩溃。相反地,使用事件代理,你只需要把一个事件处理器添加到table元素上就可以了,这个函数可以把点击事件给截下来,并且判断出是哪个单元格被点击了。

用代码写出来是什么样呢?

     代码很简单,我们所要关心的只是如何检测目标元素而已。比方说我们有一个table元素,ID是“report”,我们为这个表格添加一个事件处理器以调用editCell函数。editCell函数需要判断传到table来的事件的目标元素。考虑到我们要写的几个函数中都有可能用到这一功能,所以我们把它单独放到一个名为getEventTarget的函数中:

function getEventTarget(e) {
  e = e || window.event;
  return e.target || e.srcElement;
}

e这个变量表示的是一个事件对象,我们只需要写一点点跨浏览器的代码来返回目标元素,在IE里目标元素放在srcElemnt属性中,而在其它浏览器里则是target属性。

接下来就是editCell函数了,这个函数调用到了getEventTarget函数。一旦我们得到了目标元素,剩下的事情就是看看它是否是我们所需要的那个元素了。

function editCell(e) 

{
           var target = getEventTarget(e);
           if(target.tagName.toLowerCase() =='td') 

           {
                // DO SOMETHING WITH THE CELL
          }
}

在editCell函数中,我们通过检查目标元素标签名称的方法来确定它是否是一个表格的单元格。这种检查也许过于简单了点;如果它是这个目标元素单元格里的另一个元素呢?我们需要为代码做一点小小的修改以便于其找出父级的td元素。如果说有些单元格不需要被编辑怎么办呢?此种情况下我们可以为那些不可编辑的单元格添加一个指定的样式名称,然后在把单元格变成可编辑状态之前先检查它是否不包含那个样式名称。选择总是多样化的,你只需找到适合你应用程序的那一种。

有哪些优点和缺点呢?

       JavaScript事件代理带来的好处有:

那些需要创建的以及驻留在内存中的事件处理器少了。这是很重要的一点,这样我们就提高了性能,并降低了崩溃的风险。 
在DOM更新后无须重新绑定事件处理器了。如果你的页面是动态生成的,比如说通过Ajax,你不再需要在元素被载入或者卸载的时候来添加或者删除事件处理器了。 
潜在的问题也许并不那么明显,但是一旦你注意到这些问题,你就可以轻松地避免它们:

你的事件管理代码有成为性能瓶颈的风险,所以尽量使它能够短小精悍。 

不是所有的事件都能冒泡的。blur、focus、load和unload不能像其它事件一样冒泡 。事实上blur和focus可以用事件捕获而非事件冒泡的方法获得(在IE之外的其它浏览器中)。 
在管理鼠标事件的时候有些需要注意的地方。如果你的代码处理mousemove事件的话你遇上性能瓶颈的风险可就大了,因为mousemove事件触发非常频繁。而mouseout则因为其怪异的表现而变得很难用事件代理来管理。 


总结:
      已经有一些使用主流类库的事件代理示例出现了,比如说jQuery、Prototype以及Yahoo! UI。你也可以找到那些不用任何类库的例子,比如说Usable Type blog上的这一个。一旦需要的话,事件代理将是你工具箱里的一件得心应手的工具,而且它很容易实现。


为什么要使用事件代理?

众所周知,DOM操作是十分消耗性能的。所以重复的事件绑定简直是性能杀手。而事件代理的核心思想,就是通过尽量少的绑定,去监听尽量多的事件。

下面将会用 Zepto 为大家演示怎么实现事件代理。

啊?Zepto是什么?

 

Zepto is a minimalist JavaScript library for modern browsers with a largely jQuery-compatible API. If you use jQuery, you already know how to use Zepto.

由于API是兼容 jQuery 的,熟悉jQuery的童鞋使用 Zepto 几乎无需学习成本。演示代码实际上也能在 jQuery 上正常运行。而目前 Zepto 的适用场景更多是在移动端,一个远离旧版IE的世界。

那为什么直接用jQuery?

因为下文会简要分析源码,但jQuery里面为了兼容旧版IE做了很多妥协,源码十分不直观。而 Zepto 是面向现代浏览器设计的,所用到的API绝大多数都符合W3C标准,分析起来更加直观。

以这个HTML结构为例:

1
2
3
4
5
6
<ul >
     <li class= "list_items" >000</li>
     <li class= "list_items" >111 <a href= "javascript:void(0);" >link</a></li>
     <li class= "list_items" >222 <i>italic</i></li>
     <li>333</li>
</ul>

一般情况下,会这样绑定事件:

1
2
3
4
$( '.list_items' ).on( 'click' , function (e) {
     console.log(e.target.tagName);
     console.log( this .tagName);
});

Deom>>

打开浏览器的 Console 看一下,会发现每一个 .list_items 元素都被绑定了click事件,并且绑定的对象是 li.list_items。如下图:

这意味着,Zepto的实际上是遍历了所有 .list_items元素,并逐个绑定 click 事件。

实现思路和下面这段原生 JavaScript 代码相同:

1
2
3
4
5
6
[].forEach.call(document.querySelectorAll( '.list_items' ), function (elem) {
   elem.addEventListener( 'click' , function (e) {
     console.log(e.target.tagName);
     console.log( this .tagName);
   }, false );
});

这样的做法,当遇到数量超长的列表(ul)和表格(table)时性能开销非常大。

例如:ul 中有1000个 li 时,就需要进行1000次的事件绑定。

而事件代理,就是应用于这种场景的。

我们先看一下Zepto官方的API文档:

on 方法还支持在 回调函数 前传入一个 [selector] 的值,而这个[selector] 就是实际需要监听事件的的元素。

看看实际例子:

1
2
3
4
$( '.list' ).on( 'click' , '.list_items' , function (e) {
     console.log(e.target.tagName);
     console.log( this .tagName);
});

Demo>>

通过 on 方法把 click 事件代理在 ul.list 元素上,监听其所有 .list_items的子元素的点击操作。

打开浏览器的 Console 看看:

虽然提示每个.list_items 都有事件监听,但它们的绑定对象都是指向ul.list 。

事件代理是通过什么机制实现的?

这其中的核心思想是 事件冒泡(Event Bubble) 。但在这里我不打算细说事件冒泡,因为这会是一个很大的话题。Google一下,就能找到有很多相关的介绍了。

下面我们看一下 Zepto 的源码,它是怎么去处理这个事件代理的。

方法 on 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$.fn.on = function (event, selector, data, callback, one){
   var autoRemove, delegator, $ this = this
   if (event && !isString(event)) {
     $.each(event, function (type, fn){
       $ this .on(type, selector, data, fn, one)
     })
     return $ this
   }
 
   if (!isString(selector) && !isFunction(callback) && callback !== false )
     callback = data, data = selector, selector = undefined
   if (isFunction(data) || data === false )
     callback = data, data = undefined
 
   if (callback === false ) callback = returnFalse
 
   return $ this .each( function (_, element){
     if (one) autoRemove = function (e){
       remove(element, e.type, callback)
       return callback.apply( this , arguments)
     }
 
     if (selector) delegator = function (e){
       var evt, match = $(e.target).closest(selector, element).get(0)
       if (match && match !== element) {
         evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element})
         return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
       }
     }
 
     add(element, event, callback, data, selector, delegator || autoRemove)
   })
}

代码的大致执行流程如下:

由上面的活动图可以看出,大部分逻辑都是用来处理 on 方法传入参数,delegator 函数是事件代理的关键。

至于 add 方法,主要是对某些特殊事件进行处理,例如:ready、hover等。当成黑盒去看待,它就等同于原生的 addEventListener 方法。

下面再细分一下delegator 函数的实现逻辑:

到这里,大家应该都清楚了。

调用 on 方法进行事件绑定时,只有传入 [selector] 参数才会实现事件代理。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值