JavaScript 自定义事件实现与解耦应用

作者:白露

时间:2026-10-27

浏览:294

本文深入解析 JavaScript 中自定义事件的实现原理与应用。通过 Dialog 组件示例,展示如何利用回调机制解决组件间耦合问题,并进一步介绍基于 EventTarget 类的标准事件管理模式。文章涵盖 addHandler、removeHandler 和 trigger 方法的实现,结合寄生式组合继承,帮助开发者构建高内聚、低耦合的前端架构。

文章目录

JavaScript 自定义事件介绍

许多原生 DOM 对象都内置了事件支持,例如 div 元素拥有 click、mouseover 等事件。这种事件机制能为类的设计带来极大的灵活性,熟悉 .NET 开发的程序员对此应有深刻体会。随着 Web 技术的演进,使用 JavaScript 创建自定义对象变得日益频繁。若能让自定义对象也具备事件机制,通过事件进行外部通信,将显著提升开发效率。

简单的事件需求场景

简单的事件需求场景

事件机制并非可有可无,在某些特定需求下是不可或缺的。以 Web 开发中常见的 Dialog(对话框)为例,每个 Dialog 通常包含一个关闭按钮,该按钮对应 Dialog 的关闭逻辑。其代码结构大致如下所示。

代码如下:

<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<style type="text/css" >
.dialog
            {
position:fixed;
width:300px;
height:300px; z-index:30;
top:50%; left:50%;
margin-top:-200px; margin-left:-200px;
box-shadow:2px 2px 4px #ccc;
background-color:#f1f1f1;
display:none;
            } 
.dialog .title
           { 
font-size:16px;
font-weight:bold;
color:#fff;
padding:4px;
background-color:#404040;
            } 
.dialog .close
           { 
width:20px;
height:20px;
margin:3px;
float:right;
cursor:pointer;
            } 
</style>
</head>
<body>
<inputtype="button" value="Dialog Test" onclick="openDialog();"/>
<divid="dlgTest" class="dialog">
<imgclass="close" alt="" src="images/close.png">
<divclass="title">Dialog</div>
<divclass="content">
</div>
</div>
<scripttype="text/javascript">
function Dialog(id){
this.id=id;
var that=this;
document.getElementById(id).children[0].onclick=function(){
that.close();
            } 
        } 
Dialog.prototype.show=function(){
var dlg=document.getElementById(this.id);
dlg.style.display='block';
dlg=null;
        } 
Dialog.prototype.close=function(){
var dlg=document.getElementById(this.id);
dlg.style.display='none';
dlg=null;
        } 
</script>
<scripttype="text/javascript">
function openDialog(){
var dlg=new Dialog('dlgTest');
dlg.show();
        } 
</script>
</body>
<html>

点击按钮即可弹出 Dialog,点击关闭按钮则隐藏 Dialog,这看似实现了基本需求,但总感觉有所欠缺。通常,Dialog 显示时页面会覆盖一层半透明的灰色遮罩,以阻止用户操作页面其他部分;当 Dialog 隐藏时,遮罩也应随之移除。为此,我们需要添加相关代码来实现遮罩功能。

在 body 顶部添加一个 pageCover 元素

<div id="pageCover" class="pageCover"></div>

为其添加样式

代码如下:

.pageCover
            {
width:100%;
height:100%;
position:absolute;
z-index:10;
background-color:#666;
opacity:0.5;
display:none;
            }

为了在打开 Dialog 时显示遮罩,需要修改 openDialog 方法

代码如下:

function openDialog(){
var dlg=new Dialog('dlgTest');
document.getElementById('pageCover').style.display='block';
dlg.show();
        }


JavaScript 自定义事件介绍

效果看起来不错,半透明遮罩在 Dialog 弹出后覆盖了页面按钮,Dialog 位于遮罩之上。然而问题随之而来:关闭 Dialog 时,pageCover 依然存在,因为没有代码将其隐藏。虽然可以在打开时显示遮罩的逻辑中处理,但关闭 Dialog 是 Dialog 控件自身的逻辑,与页面无关。能否修改 Dialog 的 close 方法呢?这也不可行,原因有二:首先,Dialog 定义时并不知道 pageCover 的存在,两者之间不应存在耦合;若将隐藏逻辑写入 close 方法,Dialog 将依赖于 pageCover,导致页面无遮罩时 Dialog 出错。其次,Dialog 定义时也不知晓特定页面的 pageCover ID,无法确定隐藏哪个 div。即便在构造 Dialog 时传入 pageCover ID,解决了依赖问题,但如果某些页面需要遮罩而另一些不需要,该如何处理?

此时,事件机制便大显身手。我们需要修改 dialog 对象和 openDialog 方法

代码如下:

function Dialog(id){
this.id=id;
this.close_handler=null;
var that=this;
document.getElementById(id).children[0].onclick=function(){
that.close();
if(typeof that.close_handler=='function')
                {
that.close_handler();
                }
            }
        }

代码如下:

function openDialog(){
var dlg=new Dialog('dlgTest');
document.getElementById('pageCover').style.display='block';
dlg.close_handler=function(){
document.getElementById('pageCover').style.display='none';
            }
dlg.show();
        }

在 Dialog 对象内部添加一个句柄,关闭按钮的 click 事件处理程序在调用 close 方法后,判断该句柄是否为函数,若是则调用执行。在 openDialog 方法中,创建 Dialog 对象后对句柄赋值为隐藏 pageCover 的方法。这样,在关闭 Dialog 时即可隐藏 pageCover,同时未造成两个控件之间的耦合。这一交互过程即一个简单的定义事件——绑定事件处理程序——触发事件的过程,DOM 对象的事件(如 button 的 click 事件)也是类似原理。

高级一点的自定义事件

高级一点的自定义事件

上述示例较为简单,远不及 DOM 本身事件精细,这种简单的事件处理存在诸多弊端。

1. 缺乏通用性。若定义其他控件,还需编写一套类似的处理结构。

2. 事件绑定具有排斥性。只能绑定一个 close 事件处理程序,绑定新处理程序会覆盖之前的绑定。

3. 封装不够完善。若用户不知晓存在 close_handler 句柄,则无法绑定该事件,只能查阅源代码。

逐一分析这些弊端:对于缺乏通用性的问题,熟悉面向对象的同学可轻易想到解决方法——继承;针对事件绑定的排斥性,可提供一个容器(如二维数组)来统一管理所有事件;解决封装不完善的问题,需结合通用性,在自定义的事件管理对象中添加统一接口,用于添加、删除和触发事件。

代码如下:

function EventTarget(){
this.handlers={};
        }
EventTarget.prototype={
constructor:EventTarget,
addHandler:function(type,handler){
if(typeof this.handlers[type]=='undefined'){
this.handlers[type]=new Array();
                }
this.handlers[type].push(handler);
            },
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(handler[i]==handler){
handlers.splice(i,1);
break;
                        }
                    }
                }
            },
trigger: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);
                    }
                }
            }
        }

addHandler 方法用于添加事件处理程序,removeHandler 方法用于移除事件处理程序,所有事件处理程序在 handlers 属性中统一存储管理。调用 trigger 方法可触发一个事件,该方法接收一个至少包含 type 属性的对象作为参数,触发时会查找 handlers 属性中对应 type 的事件处理程序。以下代码用于测试。

代码如下:

function onClose(event){
alert('message:'+event.message);
        }
var target=new EventTarget();
target.addHandler('close',onClose);
//浏览器不能帮我们创建事件对象了,自己创建一个
var event={
type:'close',
message:'Page Cover closed!'
        };
target.trigger(event);

至此解决了前两个弊端,应用继承可解决第一个弊端。以下是寄生式组合继承的核心代码,这种继承方式是目前公认的 JavaScript 最佳继承方式。

代码如下:

function extend(subType,superType){
var prototype=Object(superType.prototype);
prototype.constructor=subType;
subType.prototype=prototype;
        }


最终完成的版本如下所示

代码如下:

<!DOCTYPE html>
<html>
<head>
<title>Test</title>
<style type="text/css" >
html,body
            {
height:100%;
width:100%;
padding:0;
margin:0;
            }
.dialog
            {
position:fixed;
width:300px;
height:300px;
top:50%;
left:50%;
margin-top:-200px;
margin-left:-200px;
box-shadow:2px 2px 4px #ccc;
background-color:#f1f1f1;
z-index:30;
display:none;
            }
.dialog .title
            {
font-size:16px;
font-weight:bold;
color:#fff;
padding:4px;
background-color:#404040;
            }
.dialog .close
            {
width:20px;
height:20px;
margin:3px;
float:right;
cursor:pointer;
            }
.pageCover
            {
width:100%;
height:100%;
position:absolute;
z-index:10;
background-color:#666;
opacity:0.5;
display:none;
            }
</style>
</head>
<body>
<div id="pageCover" class="pageCover"></div>
<input type="button" value="Dialog Test" onclick="openDialog();"/>
<div id="dlgTest" class="dialog">
<img class="close" alt="" src="images/close.png">
<div class="title">Dialog</div>
<div class="content">
</div>
</div>
<script type="text/javascript">
function EventTarget(){
this.handlers={};
        }
EventTarget.prototype={
constructor:EventTarget,
addHandler:function(type,handler){
if(typeof this.handlers[type]=='undefined'){
this.handlers[type]=new Array();
                }
this.handlers[type].push(handler);
            },
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(handler[i]==handler){
handlers.splice(i,1);
break;
                        }
                    }
                }
            },
trigger: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);
                    }
                }
            }
        }
</script>
<script type="text/javascript">
function extend(subType,superType){
var prototype=Object(superType.prototype);
prototype.constructor=subType;
subType.prototype=prototype;
        }
</script>
<script type="text/javascript">
function Dialog(id){
EventTarget.call(this)
this.id=id;
var that=this;
document.getElementById(id).children[0].onclick=function(){
that.close();
            }
        }
extend(Dialog,EventTarget);
        
Dialog.prototype.show=function(){
var dlg=document.getElementById(this.id);
dlg.style.display='block';
dlg=null;
        }
Dialog.prototype.close=function(){
var dlg=document.getElementById(this.id);
dlg.style.display='none';
dlg=null;
this.trigger({type:'close'});
        }
</script>
<script type="text/javascript">
function openDialog(){
var dlg=new Dialog('dlgTest');
dlg.addHandler('close',function(){
document.getElementById('pageCover').style.display='none';
            });
document.getElementById('pageCover').style.display='block';
dlg.show();
        }
</script>
</body>
<html>

总结

最后

解决了上述弊端后,代码看起来更加完善。实际上,打开 Dialog 显示 pageCover 的逻辑也可以写成类似关闭时事件的方式。当代码中存在多个部分在特定时刻相互交互的情况下,自定义事件显得尤为有用。如果每个对象都持有其他对象的引用,代码将高度耦合,对象改动会影响其他对象,导致维护困难。自定义事件使对象解耦,功能隔绝,从而实现对象之间的高聚合。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章