事件

在浏览器脚本编程中,另一块充满兼容性问题并且带来很多不愉快的区域就是浏览器事件,比如clickmouseover等等。同样的,一个JavaScript库可以解决支持IE(9以下)和W3C标准实现带来的双倍工作量。

我们来看一下一些主要的点,因为你在做一些简单的页面或者快速开发的时候可能不会使用已有的库,当然,也有可能你正在写你自己的库。

事件处理

麻烦是从给元素绑定事件开始的。假设你有一个按钮,点击它的时候增加计数器的值。你可以添加一个内联的onclick属性,这在所有的浏览器中都能正常工作,但是会违反分离和渐进增强的思想。所以你应该尽量在JavaScript中来做绑定,而不是在标签中。

假设你有下面的标签:

  1. <button id="clickme">Click me: 0</button>

你可以将一个函数赋给节点的onclick属性,但你只能这样做一次:

  1. // 不好的解决方案
  2. var b = document.getElementById('clickme'),
  3. count = 0;
  4. b.onclick = function () {
  5. count += 1;
  6. b.innerHTML = "Click me: " + count;
  7. };

如果你希望在按钮点击的时候执行好几个函数,那么在保持松耦合的情况下就不能用这种方法来做绑定。从技术上讲,你可以检测onclick是否已经包含一个函数,如果已经包含,就将它加到你自己的函数中,然后替换onclick的值为你的新函数。但是一个更干净的解决方案是使用addEventListener()方法。这个方法在IE8及以下版本中不存在,在这些浏览器中需要使用attachEvent()

当我们回头看条件初始化模式(第四章)时,会发现其中的一个示例实现就是一个很好的解决跨浏览器事件监听的套件。现在我们不讨论细节,只看一下如何给我们的按钮绑定事件:

  1. var b = document.getElementById('clickme');
  2. if (document.addEventListener) { // W3C
  3. b.addEventListener('click', myHandler, false);
  4. } else if (document.attachEvent) { // IE
  5. b.attachEvent('onclick', myHandler);
  6. } else { // 为保险起见……
  7. b.onclick = myHandler;
  8. }

现在当按钮被点击时,myHandler()会被执行。我们来让这个函数实现增加按钮文字“Click me: 0”中的数字的功能。为了更有趣一点,我们假设有好几个按钮,一个myHandler()函数来处理所有的按钮点击。如果我们可以从每次点击的事件对象中获取节点和节点对应的计数器值,那为每个按钮保持一个引用和计数器就显得不高效了。

我们先看一下解决方案,稍后再来做些评论:

  1. function myHandler(e) {
  2. var src, parts;
  3. // 获取事件对象和事件来源
  4. e = e || window.event;
  5. src = e.target || e.srcElement;
  6. // 真正工作的部分:更新文字
  7. parts = src.innerHTML.split(": ");
  8. parts[1] = parseInt(parts[1], 10) + 1;
  9. src.innerHTML = parts[0] + ": " + parts[1];
  10. // 阻止冒泡
  11. if (typeof e.stopPropagation === "function") {
  12. e.stopPropagation();
  13. }
  14. if (typeof e.cancelBubble !== "undefined") {
  15. e.cancelBubble = true;
  16. }
  17. // 阻止默认行为
  18. if (typeof e.preventDefault === "function") {
  19. e.preventDefault();
  20. }
  21. if (typeof e.returnValue !== "undefined") {
  22. e.returnValue = false;
  23. }
  24. }

在线的例子可以在http://jspatterns.com/book/8/click.html找到。

在这个事件处理函数中,有四个部分:

  • 首先,我们需要访问事件对象,它包含事件的一些信息以及触发这个事件的页面元素。事件对象会被传到事件处理回调函数中,但是使用onclick属性时需要使用全局属性window.event来获取
  • 第二部分是真正用于更新文字的部分
  • 接下来是阻止事件冒泡。在这个例子中它不是必须的,但通常情况下,如果你不阻止的话,事件会一直冒泡到文档根元素甚至window对象。同样的,我们也需要用两种方法来阻止冒泡:W3C标准方式(stopPropagation())和IE的方式(使用cancelBubble
  • 最后,如果需要的话,阻止默认行为。有一些事件(点击链接、提交表单)有默认的行为,但你可以使用preventDefault()(IE是通过设置returnValue的值为false的方式)来阻止这些默认行为

如你所见,这里涉及到了很多重复性的工作,所以使用第七章讨论过的外观模式创建自己的事件处理套件是很有意义的。

事件委托

事件委托是通过事件冒泡来实现的,它可以减少分散到各个节点上的事件处理函数的数量。如果有10个按钮在一个div元素中,你可以给div绑定一个事件处理函数,而不是给每个按钮都绑定一个。

我们来看一个实例,三个按钮放在一个div元素中(图8-1)。你可以在http://jspatterns.com/book/8/click-delegate.html看到这个事件委托的实例。

译注: 上面的URL中的例子在IE下单击会没有反应,问题在于使用document.attachEvernt()时传递的第一个参数应该是'onclick',而不是'click'

图8-1 事件委托示例:三个在点击时增加计数器值的按钮

图8-1 事件委托示例:三个在点击时增加计数器值的按钮

结构是这样的:

  1. <div id="click-wrap">
  2. <button>Click me: 0</button>
  3. <button>Click me too: 0</button>
  4. <button>Click me three: 0</button>
  5. </div>

你可以给包裹按钮的div绑定一个事件处理函数,而不是给每个按钮绑定一个。然后你可以使用和前面的示例中一样的myHandler()函数,但需要修改一个小地方:你需要将你不感兴趣的点击排除掉。在这个例子中,你只关注按钮上的点击,而在同一个div中产生的其它的点击应该被忽略掉。

myHandler()的改变就是检查事件来源的nodeName是不是"button":

  1. // ……
  2. // 获取事件对象和事件来源
  3. e = e || window.event;
  4. src = e.target || e.srcElement;
  5. if (src.nodeName.toLowerCase() !== "button") {
  6. return;
  7. }
  8. // ...

事件委托的坏处是筛选容器中感兴趣的事件使得代码看起来更多了,但好处是性能的提升和更干净的代码,这个好处明显大于坏处,因此这是一种强烈推荐的模式。

主流的JavaScript库通过提供方便的API的方式使得使用事件委托变得很容易。比如YUI3中有Y.delegate()方法,它允许你指定两个CSS选择器,一个用来匹配包裹容器,一个用来匹配你感兴趣的节点。这很方便,因为如果事件发生在你不关心的元素上时,你的事件处理回调函数不会被调用。在这种情况下,绑定一个事件处理函数很简单:

  1. Y.delegate('click', myHandler, "#click-wrap", "button");

感谢YUI抽象了浏览器的差异,已经处理好了事件的来源,使得回调函数更简单了:

  1. function myHandler(e) {
  2. var src = e.currentTarget,
  3. parts;
  4. parts = src.get('innerHTML').split(": ");
  5. parts[1] = parseInt(parts[1], 10) + 1;
  6. src.set('innerHTML', parts[0] + ": " + parts[1]);
  7. e.halt();
  8. }

你可以在http://jspatterns.com/book/8/click-y-delegate.html看到实例。