本文详细解析 JavaScript 中自定义事件的两种封装实现方式。首先介绍基于对象直接量的方法,通过 handlers 映射表存储事件类型与处理函数的数组关系,支持多监听器注册与触发,但存在属性覆盖风险。随后探讨基于对象原型链的封装方案,利用 prototype 提升安全性与复用性。文章包含完整的代码示例、调用方法及优缺点对比,帮助开发者理解并应用自定义事件机制。
JavaScript 自定义事件概述
在 DOM 操作过程中,开发者经常使用 onclick、onmouseover 等浏览器内置的特定行为事件。所谓自定义事件,即由开发者自行定义事件类型及对应的处理函数,并在需要时调用相应的处理程序。
浏览器默认事件回顾
1. JavaScript 支持的浏览器默认事件
浏览器特定行为事件、系统事件或 JS 默认事件,这些概念指代相同内容。关于 JS 默认事件的事件绑定与移除操作,开发者通常已熟悉,例如以下示例:
//DOM0级事件处理程序
var oDiv = document.getElementById('oDiv');
oDiv.onclick = function(){
alert("你点击了我");
}
或者采用另一种方式:
//DOM2级事件处理程序
var oDiv = document.getElementById('oDiv');
//非ie
oDiv.addEventListener("click",function(){
alert("你点击了我");
},false);
//ie
oDiv.attachEvent("onclick", function(){
alert("你点击了我");
});
此处不再对默认事件进行深入探讨,因为本文重点在于 JS 自定义事件。以下提供一段之前封装的用于处理 JS 默认事件的代码供参考:
//跨浏览器的事件处理程序
//调用时候直接用domEvent.addEvent( , , );直接调用
//使用时候,先用addEvent添加事件,然后在handleFun里面直接写其他函数方法,如getEvent;
//addEventListener和attachEvent---都是dom2级事件处理程序
var domEvent = {
//element:dom对象,event:待处理的事件,handleFun:处理函数
//事件名称,不含“on”,比如“click”、“mouseover”、“keydown”等
addEvent:function(element,event,handleFun){
//addEventListener----应用于mozilla
if(element.addEventListener){
element.addEventListener(event,handleFun,false);
}//attachEvent----应用于IE
else if(element.attachEvent){
element.attachEvent("on"+event,handleFun);
}//其他的选择dom0级事件处理程序
else{
//element.onclick===element["on"+event];
element["on"+event] = handleFun;
}
},
//事件名称,含“on”,比如“onclick”、“onmouseover”、“onkeydown”等
removeEvent:function(element,event,handleFun){
//removeEventListener----应用于mozilla
if (element.removeEventListener) {
element.removeEventListener(event,handleFun,false);
}//detachEvent----应用于IE
else if (element.detachEvent) {
element.detachEvent("on"+event,handleFun);
}//其他的选择dom0级事件处理程序
else {
element["on"+event] = null;
}
},
//阻止事件冒泡
stopPropagation:function(event){
if(event.stopPropagation){
event.stopPropagation();
}else{
event.cancelBubble = true;//IE阻止事件冒泡,true代表阻止
}
},
//阻止事件默认行为
preventDefault:function(event){
if(event.preventDefault){
event.preventDefault();
}else{
event.returnValue = false;//IE阻止事件冒泡,false代表阻止
}
},
//获得事件元素
//event.target--非IE
//event.srcElement--IE
getElement:function(event){
return event.target || event.srcElement;
},
//获得事件
getEvent:function(event){
return event? event : window.event;
},
//获得事件类型
getType:function(event){
return event.type;
}
};
接下来进入正题,探讨 JS 自定义事件的实现。
对象直接量封装方案
2. 对象直接量封装 JS 自定义事件
基于上述封装思路,我们可以进行如下构思:
var eventTarget = {
addEvent: function(){
//添加事件
},
fireEvent: function(){
//触发事件
},
removeEvent: function(){
//移除事件
}
};
相信这种思路易于理解。但随之产生一个问题:JS 默认事件由系统一一对应,而自定义事件的映射关系需由开发者自行建立。具体构建方式如下:
var eventTarget = {
//保存映射
handlers:{},
addEvent: function(){
//处理代码
},
fireEvent: function(){
//触发代码
},
removeEvent: function(){
//移出代码
}
};
构建映射关系的代码如下:
handlers = {
"type1":[
"fun1",
"fun2",
// "..."
],
"type2":[
"fun1",
"fun2"
// "..."
]
//"..."
}
如此设计,每个事件类型可关联多个处理函数,便于后续扩展。接下来是具体的代码实战部分。
思路已清晰,直接附上实现代码:
//直接量处理js自定义事件
var eventTarget = {
//保存事件类型,处理函数数组映射
handlers:{},
//注册给定类型的事件处理程序,
//type -> 自定义事件类型, handler -> 自定义事件回调函数
addEvent: function(type, handler){
//判断事件处理数组是否有该类型事件
if(eventTarget.handlers[type] == undefined){
eventTarget.handlers[type] = [];
}
//将处理事件push到事件处理数组里面
eventTarget.handlers[type].push(handler);
},
//触发一个事件
//event -> 为一个js对象,属性中至少包含type属性,
//因为类型是必须的,其次可以传一些处理函数需要的其他变量参数。(这也是为什么要传js对象的原因)
fireEvent: function(event){
//判断是否存在该事件类型
if(eventTarget.handlers[event.type] instanceof Array){
var _handler = eventTarget.handlers[event.type];
//在同一个事件类型下的可能存在多种处理事件,找出本次需要处理的事件
for(var i = 0; i < _handler.length; i++){
//执行触发
_handler[i](event);
}
}
},
//注销事件
//type -> 自定义事件类型, handler -> 自定义事件回调函数
removeEvent: function(type, handler){
if(eventTarget.handlers[type] instanceof Array){
var _handler = eventTarget.handlers[type];
//在同一个事件类型下的可能存在多种处理事件,找出本次需要处理的事件
for(var i = 0; i < _handler.length; i++){
//找出本次需要处理的事件下标
if(_handler[i] == handler){
break;
}
}
//删除处理事件
_handler.splice(i, 1);
}
}
};
这是一种调用运行的方法:
eventTarget.addEvent("eat",function(){
console.log(123); //123
});
eventTarget.fireEvent({type: "eat"});
该方法存在一个缺点:无法删除已注册的处理事件。这是因为使用了映射表存储数据,且不建议在映射表中存入过多数据。
另一种方法是将处理事件提取出来(推荐):
function b(){
console.log(123);
}
eventTarget.addEvent("eat",b);
eventTarget.fireEvent({
type: "eat"
}); //123
eventTarget.removeEvent("eat",b);
eventTarget.fireEvent({type: "eat"}); //空
也可以传递更多参数:
eventTarget.fireEvent({
type: "eat",
food: "banana"
});
function b(data){
console.log(data.food); //banana
}
总结:字面量方法存在一定缺陷,若不慎在 handler 函数中将某个属性赋值为 null,可能导致 eventTarget 方法崩溃。相比之下,原型链封装更为安全。
对象原型链封装方案
3. 对象原型封装 JS 自定义事件
由于前文思路已阐述清楚,此处直接附上代码。读者可自行研究其利弊,或寻找更优解决方案:
//自定义事件构造函数
function EventTarget(){
//事件处理程序数组集合
this.handlers = {};
}
//自定义事件的原型对象
EventTarget.prototype = {
//设置原型构造函数链
constructor: EventTarget,
//注册给定类型的事件处理程序,
//type -> 自定义事件类型, handler -> 自定义事件回调函数
addEvent: function(type, handler){
//判断事件处理数组是否有该类型事件
if(typeof this.handlers[type] == 'undefined'){
this.handlers[type] = [];
}
//将处理事件push到事件处理数组里面
this.handlers[type].push(handler);
},
//触发一个事件
//event -> 为一个js对象,属性中至少包含type属性,
//因为类型是必须的,其次可以传一些处理函数需要的其他变量参数。(这也是为什么要传js对象的原因)
fireEvent: function(event){
//模拟真实事件的event
if(!event.target){
event.target = this;
}
//判断是否存在该事件类型
if(this.handlers[event.type] instanceof Array){
var handlers = this.handlers[event.type];
//在同一个事件类型下的可能存在多种处理事件,找出本次需要处理的事件
for(var i = 0; i < handlers.length; i++){
//执行触发
handlers[i](event);
}
}
},
//注销事件
//type -> 自定义事件类型, handler -> 自定义事件回调函数
removeEvent: function(type, handler){
//判断是否存在该事件类型
if(this.handlers[type] instanceof Array){
var handlers = this.handlers[type];
//在同一个事件类型下的可能存在多种处理事件
for(var i = 0; i < handlers.length; i++){
//找出本次需要处理的事件下标
if(handlers[i] == handler){
break;
}
}
//从事件处理数组里面删除
handlers.splice(i, 1);
}
}
};
调用方法如下:
function b(){
console.log(123);
}
var target = new EventTarget();
target.addEvent("eat", b);
target.fireEvent({
type: "eat"
}); //123
原型链方法与对象直接量方法功能一致。
以上就是本文的全部内容,希望对大家的学习有所帮助。