JavaScript 组件事件

作者:Greg Murray

问题描述

事件在 JavaScript 组件中至关重要,因为它们驱动用户接口,产生 AJAX 请求并使 JavaScript 组件彼此之间能够进行交互。很难从头开始编写跨浏览器的事件处理代码,因为 JavaScript 中包含多种不同的事件处理方法,并且每种浏览器存在其特定的技巧和问题。

随着页面中的 JavaScript 组件数量的增加,组件代码会越来越紧密地耦合在一起。这并不是一种开发用户接口的有效方法,因为产生的代码可重用性较差,并且难以进行管理和维护。您需要寻找一种有效方法,使 JavaScript 组件不“紧密耦合”,并且能够在将其用于其他组件时使用最少的“粘合”代码。

解决方案

使用 Dojo 库;该库抽象出 JavaScript 事件系统,提供了一组用于事件处理的 JavaScript API 和一种进行组件间事件通信的方法。Dojo 提供了一些备选方法来处理事件,其中包括简单的事件处理程序、事件侦听程序以及发布/订阅事件。Dojo 事件处理 API 并不是互斥的,在很多情况下,可以根据情况组合使用这些 API。现在,让我们详细介绍一下这些使用情况和 API。

下面详细说明了每个 API 及其适用的使用情况。

简单事件处理程序

使用 dojo.event.connect() 在 JavaScript 组件中注册事件处理程序,而不是使用 DOM 或基于属性的事件处理程序,因为 dojo.event.connect() 提供了一致的 API,抽象出浏览器差异以及防止某些浏览器中出现的内存泄漏。该 API 还负责实现将多个事件处理程序连接到单个事件类型上的细节。

通过使用 dojo.event.connect(),您可以将一个或多个事件连接到某个对象上。这些事件是按添加顺序调用的。

添加一个 before(之前)侦听程序所需的参数有:

dojo.event.connect(srcObj, "srcFunc", "targetFunc")

下面是一个连接事件处理程序的示例:

<script type="text/javascript" src="dojo.js"></script>
<script type="text/javascript">
window.onload = function () {
  var link = document.getElementById("mylink");
  dojo.event.connect(link, "onclick", myHandler);
}

function myHandler(evt) {
    alert("dojo.connect handler");
}
</script>
<a href="#" id="mylink">Click Me</a>

以上示例中,link 元素的 "onclick" 属性映射到事件处理程序函数 myHandler 上。如果存在现有处理程序,则在调用现有处理程序之后调用 Dojo 处理程序。下面是连接匿名事件处理程序的更详细示例:

<script type="text/javascript" src="dojo.js"></script>
<script type="text/javascript">
window.onload = function () {
  var link = document.getElementById("mylink");
  // connect link element 'onclick' property to an anonymous function
  dojo.event.connect(link, "onclick", function(evt) {
     var srcElement;
     // this function is passed the browser specific mouse event
     if (evt.target) {
         srcElement = evt.target;
     } else if (evt.srcElement) {
         srcElement = evt.srcElement;
     }
     if (srcElement) {
        alert("dojo.event.connect event: " + srcElement.getAttribute("id"));
    }
  });
}"
</script>
<a href="#" id="mylink" onclick="alert('inline event');">Click Me</a>

上面的示例为 link 元素(在文档正文中使用 id "mylink" 定义)创建了一个匿名事件处理程序 (function)。如果事件处理程序不存在,则使用 dojo.event.connect 定义的事件处理程序将成为缺省处理程序。否则,如果处理程序类似于上面的内联处理程序,则在缺省内联事件之后调用 Dojo 事件。正如在示例中所看到的那样,dojo.event.connect API 提供对事件对象 (evt) 的访问,该对象是作为参数提供的。请注意,Dojo 不提供特定于浏览器的鼠标事件的抽象。上面的示例使用对象检测来获取源元素。

事件侦听程序

如果希望一个对象/组件侦听另一个对象上的事件并采取某些操作,则可以使用事件侦听程序。切记,虽然侦听程序能够访问将传递给被侦听的事件处理程序的参数,但它们只是进行侦听。

可以在源事件处理程序之前 (before) 或之后 (after) 调用一个或多个侦听程序。将为这些事件侦听程序传递与源事件处理程序相同的参数。这些侦听程序是按连接到源事件处理程序的顺序进行调用的。选择使用 "before" 或 "after" 是由应用程序的使用情况决定的。重要的是,Dojo 在使用这两个参数方面提供了很大的灵活性。

连接一个侦听程序所需的参数有:

dojo.event.connect("before/after", srcObj, "srcFunc", targetObj, "targetFunc")

第一个参数是 "before" 还是 "after" 取决于何时调用侦听程序。第二个参数是要将事件连接到的源对象(通常为 HTML 元素)。第三个参数是将被侦听的函数的名称(带有引号的字符串)。第四个参数是目标对象。第五个参数是在调用源函数之前或之后调用的目标对象的函数(带有引号的字符串),是之前还是之后调用取决于第一个参数。如果难以跟踪所有参数名称,则可以选择使用 dojo.event.kwConnect(),它用于获取一个对象类型(以参数名称和值为属性),有关详细信息,请参见 Dojo 事件系统

以下示例使用 dojo.event.connect 连接要在调用 loadMenu 事件处理程序之前调用的 loadMenuListener 函数。

  function loadMenu(args) {
      alert("args=" + args);
  }
  
  function loadMenuListener(args) {
      alert("loadMenuListener: args=" + args);
  }
  
  dojo.event.connect("before", this, "loadMenu", this, "loadMenuListener");
  loadMenu({name: "MyMenu", items: ['File', 'Save']});
  // alerts loadMenuListener: args=[object Object]
  // alerts args=args=[object Object]

如果希望在调用事件处理程序之后通知侦听程序,请在使用 dojo.event.connect 时将第一个参数指定为 "after"。

  function loadMenu(args) {
      alert("args=" + args);
  }
  
  function loadMenuListener(args) {
      alert("loadMenuListener: args=" + args);
  }
  
  dojo.event.connect("after", this, "loadMenu", this, "loadMenuListener");
  loadMenu({name: "MyMenu", items: ['File', 'Save']});
  // alerts args=[object Object]
  // alerts loadMenuListener: args=[object Object]

在本示例中,在调用源事件处理程序之后调用了侦听程序。

大多数 Java 开发者对侦听程序方法最为熟悉,因为这一方法在整个 Java 平台中被广泛使用。这种方法具有额外的好处,即可以在调用特定处理程序之前或之后调用侦听程序。

事件处理程序包装

如果需要截取并修改事件处理程序行为而不修改所用组件的 JavaScript 源代码,则可以使用 "around" 来包装事件。

添加 "before" 或 "after" 事件处理程序侦听程序可能还不够。在某种情况下,您可能需要修改事件处理程序的行为或参数,而不修改正在使用的 JavaScript 组件的源代码。在 Dojo 中,这是使用 dojo.io.connect 并将 "around" 作为第一个参数实现的。

添加一个事件包装所需的参数有:

dojo.event.connect("around", srcObj, "srcFunc", targetObj, "targetFunc")

这些参数与侦听程序方法基本相同,唯一的差别是第一个参数为 "around"。

下面的示例说明了由定制事件处理程序包装的一个对象的事件处理程序。定制事件处理程序将应用其自己的逻辑,然后调用源事件处理程序。

    // custom event handler wrapper
    function customLoadHandler(invocation) {
       alert("custom menu name =" + invocation.args[0].name);
       // update the name property of the argument
       invocation.args[0].name = "Custom Menu";
       //call the default event handler
       invocation.proceed();
    }

    function ImageScroller() {   
        this.load = function (args) {
            alert("default menu name=" + args.name);
        }
    }

    var is = new ImageScroller();
    dojo.event.connect("around", is, "load", this, "customLoadHandler");
    is.load({name: "My Menu", items: ['File', 'Save']});
    // alerts "custom menu name=My Menu"
    // alerts "default menu name=Custom Menu"    

上面的示例说明了如何编写定制代码以截取在 ImageScroller 上对公共函数 load 的调用,然后使用函数 customLoadHandler 对其进行包装。本示例中的 customLoadHandler 函数用于处理为 load 函数提供的参数。请注意,Dojo 运行环境为 customLoadHandler 函数传递一个参数(即以上示例中的 invocation)以及两个属性:"args"(传递给目标事件处理程序的参数数组)和 "proceed"(调用目标事件处理程序的函数)。

如果源函数返回一个值,则包装可能会在调用 "proceed" 时检索该值,并将其分配给一个变量。包装可能会根据其中的逻辑更改或修改返回的变量值。

对于曾经使用过 Servlet 过滤器的开发者,这种方法与其截取请求并修改行为的情况非常类似。以过滤器为例,getParameters() 相当于 invocation.argsdoFilter() 相当于 invocation.proceed()

发布/订阅事件处理

可以使用发布和订阅在组件之间匿名地进行事件通信。也可以考虑定制组件以将主题名称作为初始化参数进行传入,使组件变得更加灵活。并非所有事件处理都需要使用发布和订阅进行公开,但应尽可能提高灵活性以便将来与其他组件进行集成。

假定显示产品的组件 ImageScroller 可由另一个组件 AccordionMenu 进行设置。可通过使用 dojo.event.publishdojo.event.subscribe API 来实现此操作(如以下示例所示)。

<script type="text/javascript" src="dojo.js"></script>

<script type="text/javascript">
window.onload=init;
var ac;
var is;

function init() {
    ac = new AccordionMenu();
    ac.load();
    is = new ImageScroller();
    is.load();
}

function ImageScroller() {
    this.setProducts = function(pid) {
        // show the products for pid
    }

    this.handleEvent = function(args) {
        if (args.event == 'showProducts') {
            this.setProducts(args.value);
        }
    }
    
    this.load = function () {
        dojo.event.topic.subscribe("/scroller", this, handleEvent);
    }
}

// accordion menu defined below or in another .js file included in the page

<script>

对于上面的示例,JavaScript 组件 ImageScroller 进行注册以侦听主题 "/scroller" 上的事件。handleEvent 函数被设置为事件处理程序。请注意,handleEvent 函数是使用 'this' 定义的,它允许外部 JavaScript 代码或组件手动调用该组件的事件处理函数。本示例将具有属性 "event" 的对象类型 "args" 用于事件类型;并将 "value" 用于事件处理程序的事件值。建议您使用对象类型作为参数,因为它们具有很大的灵活性,并允许定制传递给函数的参数,而无需更改函数签名。

function AccordionMenu() {
    function expandRow(target) {
       ...
       var link = document.createElement("a");
       dojo.event.connect(link, "onclick", function(evt){
           this.target = target;
           dojo.event.topic.publish("/scroller", {event: "showProducts", value : target});
       });
    }
}

当 "link" 收到 "onclick" 事件时,事件处理程序将向主题 "/scroller" 发布一个事件,其中包含一个具有属性 "event"(属性值为 "showProducts")和属性 "value"(值为 target)的对象类型。此处将使用一个关闭以正确维护 target 属性的值,因为在调用匿名处理程序时,该值将超出范围。切记,在使用这种类型的关闭时,只要任何引用指向用于构成关闭的属性,匿名处理程序就会存在。此外,还要谨慎对待 DOM 元素引用,因为它们可能会导致内存泄漏。

断开事件处理程序

虽然本文的大部分篇幅侧重于连接到事件处理程序的使用情况,但还遗留一个问题,即不需要事件时如何将其与对象分离。要完成此操作,对于事件请调用 dojo.event.disconnect,对于主题则调用 dojo.event.unsubscribe,所使用的参数集与连接或订阅事件处理程序时使用的参数集完全相同。

在开发组件时,您可以使用其中的一种或多种事件处理方法,具体取决于所执行的操作。Dojo 提供了很多功能强大的事件处理方法。有关更多详细信息和示例,请参见下面的资源。

资源

Dojo 事件系统

Dojo 事件示例


© Sun Microsystems 2006。Java BluePrints Solutions Catalog 中的所有内容受版权保护,未经 Sun Microsystems 的明确书面许可,不得在其他产品中发布。