【翻译】谈 focus 和 blur 的事件代理
在下拉列表等许多常用的效果中,事件代理往往非常的重要,因为许多在各个链接上触发的事件,往往可以很容易的在根节点中进行监听。
然而这会引发一个问题,尽管事件代理对于鼠标事件的响应非常好,但它对于focus和blur事件却不支持。而让键盘操作支持下拉列表往往需要它们来实现。
随后,在我对事件的实验中,我发现了代理focus和blur事件的方法。
什么是事件代理
让我们先讨论一下什么是事件代理。我将用一个下拉菜单来作为我们的例子。
当用户使用鼠标操作下拉菜单时,我们需要监听用户所有的mouseover和mouseout事件,以便判断用户最后一个行为应该打开或是关闭菜单。
mouseover和mouseout事件是会冒泡的。也就是说,当一个mouseover事件在链接上产生时,该事件会在dom树上往上"爬",这样去查看链接的任意父节点是否也定义了mouseover事件。它首先会查看链接自己,随后查看其父节点<li>,接着查看<ol>,接着一直追查到document或者window节点
这意味着我们完全可以将通用的事件监听函数定义在整个菜单的根节点<ol>上。当mouseover,mousedown等事件发生在子结点时,事件的冒泡会保证我们根节点的监听函数能够捕捉到它
2 <li><a href="#">List item 1</a>
3 <ol>
4 <li><a href="#">List item 1.1</a></li>
5 <li><a href="#">List item 1.2</a></li>
6 <li><a href="#">List item 1.3</a></li>
7 </ol>
8 </li>
9 [etc.]
10 </ol>
11
12 $('dropdown').onmouseover = handleMouseOver;
13 $('dropdown').onmouseout = handleMouseOut;
这个技巧最大的好处是我们不需要在每一个链接上都定义两个事件句柄,从而可以节省浏览器内存消耗。
focus引发的问题
一切都很好。然而,当你需要让你的下拉菜单支持键盘事件的时候,你将遇到麻烦。
理论上说,让下拉菜单支持键盘事件是很容易的:你只需定义好focus事件和blur事件的事件句柄。然而,问题在于,这两个事件并不会冒泡。链接上的focus或blur事件仅发生于链接自身上,而它的任何父节点上的事件都不会产生。
这是一项很古老的规则。少数事件,例如focus,blur,change等,并不会在dom树上冒泡。这样设计准确的原因已经无从考究,但部分原因只是由于这些事件对一些元素来说是没有意义的。用户不可能更改或聚焦于一段随即的段落上。因此这些事件不支持这一些html标签。于是,它们不会发生冒泡了。
看看下面的例子:
2 Some text.
3 <input id="testInput" />
4 </p>
5
6 $('testParagraph').onfocus = handleEventPar;
7 $('testInput').onfocus = handleEventInput;
当用户聚焦于input元素上时,handleEventInput事件将被触发。然而,由于事件并不会冒泡,handleEventPar将不会执行。另外,由于我们不可能聚焦于段落上(除非它定义了tabindex属性),handleEventPar将永远不会被执行。
事件的捕获
除非我们使用事件的捕获。
事件的捕获与事件的冒泡是相反的。事件的冒泡,开始发生与触发事件节点本身,随后在dom树上上行。而事件的捕获,最开始发生于dom树的根节点(通常是document或者window),随后在dom树上下行。
在我实验中,我感到最好奇的是,不论所给定的节点是否对发生的事件有意义,所有的捕获阶段的父节点的事件句柄都会被触发。
所以,让我们做一个类似的实例。但是现在使用addEventListener来添加捕获的事件。
2 Some text.
3 <input id="testInput" />
4 </p>
5
6 $('testParagraph').addEventListener('focus',handleEventPar,true);
7 $('testInput').addEventListener('focus',handleEventInput,true);
现在如果用户聚焦于input元素,事件会在window对象或者document对象上触发,随后一直细化到input元素本身。在这个过程中,他将遇到段落上定义的onfocus句柄并且执行它,即使聚焦事件对于段落来说没有意义。
最终结果,handleEventPar先执行,然后才是handleEventInput。
IE
不幸的是ie不支持捕获事件。然而,它支持focusin和focusout这两个事件。与focus和blur相反,它们支持事件的冒泡。如果我们在ie上使用这些事件,我们就大功完成了。
focus和blur事件的代理
因此,我们的结论是,onfocus和onblur事件都可以通过注册捕获事件来获得onmouseover相同的效果。于是我们可以代理所有的键盘兼容的下拉菜单的事件了。
2 <li><a href="#">List item 1</a>
3 <ol>
4 <li><a href="#">List item 1.1</a></li>
5 <li><a href="#">List item 1.2</a></li>
6 <li><a href="#">List item 1.3</a></li>
7 </ol>
8 </li>
9 [etc.]
10 </ol>
11
12 $('dropdown').onmouseover = handleMouseOver;
13 $('dropdown').onmouseout = handleMouseOut;
14 $('dropdown').onfocusin = handleMouseOver;
15 $('dropdown').onfocusout = handleMouseOut;
16 $('dropdown').addEventListener('focus',handleMouseOver,true);
17 $('dropdown').addEventListener('blur',handleMouseOut,true);
ppk的文章总是很经典。这次做项目遇到blur不支持冒泡的问题,又在ppk这里找到了答案。搜不到有翻译的版本,想想对于很多开发者来说这样一个hack是相当有用的,于是自己翻译了出来。ppk写文章的过程中也没有解决ie的问题,后来Dean Edwards在留言中指出了解决方法。乐于分享,共同成长。