22.4 自定义事件
在本书前面,你已经学到事件是JavaScript 与浏览器交互的主要途径。事件是一种叫做观察者的设计模式,这是一种创建松散耦合代码的技术。对象可以发布事件,用来表示在该对象生命周期中某个有趣的时刻到了。然后其他对象可以观察该对象,等待这些有趣的时刻到来并通过运行代码来响应。
观察者模式由两类对象组成:主体和观察者。主体负责发布事件,同时观察者通过订阅这些事件来观察该主体。该模式的一个关键概念是主体并不知道观察者的任何事情,也就是说它可以独自存在并正常运作即使观察者不存在。从另一方面来说,观察者知道主体并能注册事件的回调函数(事件处理程序)。
涉及DOM 上时,DOM元素便是主体,你的事件处理代码便是观察者。事件是与DOM 交互的最常见的方式,但它们也可以用于非DOM 代码中——通过实现自定义事件。自定义事件背后的概念是创建一个管理事件的对象,让其他对象监听那些事件。实现此功能的基本模式可以如下定义:
function EventTarget() { this.handlers = {}; } EventTarget.prototype = { constructor: EventTarget, addHandler: function(type, handler) {if (typeof this.handlers[type] == "undefined") {this.handlers[type] = [];}this.handlers[type].push(handler); }, fire: function(event) {if (!event.target) {event.target = this;}if (this.handlers[event.type] instanceof Array) {var handlers = this.handlers[event.type];for (var i = 0,len = handlers.length; i < len; i++) {handlers[i](event);}} }, removeHandler: function(type, handler) {if (this.handlers[type] instanceof Array) {var handlers = this.handlers[type];for (var i = 0,len = handlers.length; i < len; i++) {if (handlers[i] === handler) {break;}}handlers.splice(i, 1);} } };EventTarget.js EventTarget 类型有一个单独的属性handlers,用于储存事件处理程序。还有三个方法:
- addHandler() , 用于注册给定类型事件的事件处理程序; fire() , 用于触发一个事件;
- removeHandler(),用于注销某个事件类型的事件处理程序。
- addHandler()方法接受两个参数:事件类型和用于处理该事件的函数。当调用该方法时,会进行一次检查,看看handlers 属性中是否已经存在一个针对该事件类型的数组;如果没有,则创建一个新的。然后使用push()将该处理程序添加到数组的末尾。
如果要触发一个事件,要调用fire()函数。该方法接受一个单独的参数,是一个至少包含type属性的对象。fire()方法先给event 对象设置一个target 属性,如果它尚未被指定的话。然后它就查找对应该事件类型的一组处理程序,调用各个函数,并给出event 对象。因为这些都是自定义事件,所以event 对象上还需要的额外信息由你自己决定。
removeHandler()方法是addHandler()的辅助,它们接受的参数一样:事件的类型和事件处理程序。这个方法搜索事件处理程序的数组找到要删除的处理程序的位置。如果找到了,则使用break操作符退出for 循环。然后使用splice()方法将该项目从数组中删除。然后,使用EventTarget 类型的自定义事件可以如下使用:
function handleMessage(event) { alert("Message received: " + event.message); } //创建一个新对象 var target = new EventTarget(); //添加一个事件处理程序 target.addHandler("message", handleMessage); //触发事件 target.fire({ type: "message", message: "Hello world!" }); //删除事件处理程序 target.removeHandler("message", handleMessage); //再次,应没有处理程序 target.fire({ type: "message", message: "Hello world!" });运行一下 在这段代码中,定义了handleMessage()函数用于处理message 事件。它接受event 对象并输出message 属性。调用target 对象的addHandler()方法并传给"message"以及handleMessage()函数。在接下来的一行上,调用了fire()函数,并传递了包含2 个属性,即type 和message 的对象直接量。它会调用message 事件的事件处理程序,这样就会显示一个警告框(来自handleMessage())。然后删除了事件处理程序,这样即使事件再次触发,也不会显示任何警告框。因为这种功能是封装在一种自定义类型中的,其他对象可以继承EventTarget 并获得这个行为,如下例所示:
function Person(name, age) { EventTarget.call(this); this.name = name; this.age = age; } inheritPrototype(Person, EventTarget); Person.prototype.say = function(message) { this.fire({type: "message",message: message }); };Person 类型使用了寄生组合继承(参见第6 章)方法来继承EventTarget。一旦调用了say()方法,便触发了事件,它包含了消息的细节。在某种类型的另外的方法中调用fire()方法是很常见的,同时它通常不是公开调用的。这段代码可以照如下方式使用:
function handleMessage(event) { alert(event.target.name + " says: " + event.message); } //创建新person var person = new Person("Nicholas", 29); //添加一个事件处理程序 person.addHandler("message", handleMessage); //在该对象上调用1 个方法,它触发消息事件 person.say("Hi there.");运行一下
这个例子中的handleMessage()函数显示了某人名字(通过event.target.name 获得)的一个警告框和消息正文。当调用say()方法并传递一个消息时,就会触发message 事件。接下来,它又会调用handleMessage()函数并显示警告框。
当代码中存在多个部分在特定时刻相互交互的情况下,自定义事件就非常有用了。这时,如果每个对象都有对其他所有对象的引用,那么整个代码就会紧密耦合,同时维护也变得很困难,因为对某个对象的修改也会影响到其他对象。使用自定义事件有助于解耦相关对象,保持功能的隔绝。在很多情况中,触发事件的代码和监听事件的代码是完全分离的。