18.3 自定义事件
在本书前面,你已经学到事件是 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 类型有一个单独的属性 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 object(o){ // 原型式继承
function F(){}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType){ // 寄生组合式继承
var prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 指定对象
}
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);
}
}
};
// 开始继承
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});
};
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() 函数并显示警告框。
当代码中存在多个部分在特定时刻相互交互的情况下,自定义事件就非常有用了。这时,如果每个对象都有对其他所有对象引用,那么整个代码就会紧密耦合,同时维护也变得很困难,因为对某个对象的修改也会影响到其他对象。使用自定义事件有助于解耦相关对象,保持功能的隔绝。在很多情况中,触发事件的代码和监听事件的代码是完全分离的。