设计弹出对话框
无论从事Web开发,还是从事GUI开发,事件都是经常用到的。随着Web技术的发展,使用JavaScript自定义事件愈发频繁,为创建的对象绑定事件机制,通过事件对外通信,可以极大提高开发效率。
从本节开始,我们将针对同一个项目,为了实现更加完善的功能,逐步介绍如何设计自定义事件。
【示例】事件并不是可有可无的,在某些需求下是必需的。下面示例通过简单的需求说明事件的重要性,在Web开发中对话框是很常见的组件,每个对话框都有一个关闭按钮,关闭按钮对应关闭对话框的方法。示例初步设计的完整代码如下,演示效果如图1所示。
<!DOCTYPE html>
<html>
<head>
<title></title>
<style type="text/css" >
/*对话框外框样式*/
.dialog { width: 300px; height: 200px; margin:auto; box-shadow: 2px 2px 4px #ccc; background-color: #f1f1f1; border: solid 1px #aaa; border-radius: 4px; overflow: hidden; display: none; }
/*对话框的标题栏样式*/
.dialog .title { font-size: 16px; font-weight: bold; color: #fff; padding: 6px; background-color: #404040; }
/*关闭按钮样式*/
.dialog .close { width: 20px; height: 20px; margin: 3px; float: right; cursor: pointer; color: #fff; }
</style>
<meta charset="utf-8">
</head>
<body>
<input type="button" value="打开对话框" onclick="openDialog();"/>
<div id="dlgTest" class="dialog"><span class="close">×</span>
<div class="title">对话框标题栏</div>
<div class="content">对话框内容框</div>
</div>
<script type="text/javascript">
//定义对话框类型对象
function Dialog(id){
this.id=id; //存储对话框包含框的ID
var that=this; //存储Dialog的实例对象
document.getElementById(id).children[0].onclick=function(){
that.close(); //调用Dialog的原型方法关闭对话框
}
}
//定义Dialog原型方法
//显示Dialog对话框
Dialog.prototype.show=function(){
var dlg=document.getElementById(this.id); //根据id获取对话框的DOM引用
dlg.style.display='block'; //显示对话框
dlg=null; //清空引用,避免生成闭包
}
//关闭Dialog对话框
Dialog.prototype.close=function(){
var dlg=document.getElementById(this.id); //根据id获取对话框的DOM引用
dlg.style.display='none'; //隐藏对话框
dlg=null; //清空引用,避免生成闭包
}
//定义打开对话框的方法
function openDialog(){
var dlg=new Dialog('dlgTest'); //实例化Dialog
dlg.show(); //调用原型方法,显示对话框
}
</script>
</body>
</html>

图1 打开对话框
在上面示例中,当单击页面中的“打开对话框”按钮,就可以弹出对话框,点击对话框右上角的关闭按钮,可以隐藏对话框。
设计遮罩层
一般对话框在显示的时候,页面还会弹出一层灰蒙蒙半透明的遮罩层,阻止用户对页面其它对象的操作,当对话框隐藏的时候,遮罩层会自动消失,页面又能够被操作。本节以上节示例为基础,进一步执行下面操作。
【操作步骤】
第1步,复制上一节示例文件test1.html,在<body>顶部添加一个遮罩层:
<div id="pageCover" class="pageCover"></div>
第2步,为其添加样式:
.pageCover { width: 100%; height: 100%; position: absolute; z-index: 10; background-color: #666; opacity: 0.5; display: none; }
第3步,设计打开对话框时,显示遮罩层,需要修改openDialog方法代码:
function openDialog(){
//新增的代码
//显示遮罩层
document.getElementById('pageCover').style.display='block';
var dlg=new Dialog('dlgTest');
dlg.show();
}
第4步,重新设计对话框的样式,避免被遮罩层覆盖,同时清理body的默认边距。
/*清除页边距,避免其对遮罩层的影响*/
body{ margin:0; padding:0;}
/*设计对话框固定定位显示,让其显示在覆盖层上面,并总是显示在窗口中央位置*/
.dialog { width: 300px; height: 200px;
position:fixed; /*固定定位*/
left:50%;top:50%;margin-top:-100px; margin-left:-150px; /*窗口中央显示*/
z-index: 30; /*在覆盖层上面显示*/
box-shadow: 2px 2px 4px #ccc; background-color: #f1f1f1; border: solid 1px #aaa; border-radius: 4px; overflow: hidden; display: none; }
第5步,保存文档,在浏览器中预览,则显示效果如图2所示。

图2 重新设计对话框
在上面示例中,当打开对话框后,半透明的遮罩层在对话框弹出后,遮盖住页面上的按钮,对话框在遮罩层之上。但是,当关闭对话框的时候,遮罩层仍然存在页面中,没有代码能够将其隐藏。
如果按照打开时怎么显示遮罩层,关闭时就怎么隐藏。但是,这个试验没有成功,因为显示遮罩层的代码是在页面上按钮事件处理函数中定义的,而关闭对话框的方法存在于Dialog内部,与页面无关,是不是修改Dialog的close方法就可以?也不行,仔细分析有两个原因:
首先,在定义Dialog时并不知道遮罩层的存在,这两个组件之间没有耦合关系,如果把隐藏遮罩层的逻辑写在Dialog的close方法内,那么Dialog将依赖于遮罩层的。也就是说,如果页面上没有遮罩层,Dialog就会出错。
其次,在定义Dialog时,也不知道特定页面遮罩层的ID(<div id="pageCover">),没有办法知道隐藏哪个<div>标签。
是不是在构造Dialog时,把遮罩层的ID传入就可以了呢? 这样两个组件不再有依赖关系,也能够通过ID找到遮罩层所在的<div>标签了,但是如果用户需要部分页面弹出遮罩层,部分页面不需要遮罩层,又将怎么办?即便能够实现,但是这种写法比较笨拙,代码不够简洁、灵活。
自定义事件
通过上一节示例分析说明,如果简单针对某个具体页面,所有问题都可以迎刃而解,但是如果设计适应能力强,可满足不同用户需求的对话框组件,使用自定义事件是最好的方法。
复制上节示例test1.html,修改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(){
document.getElementById('pageCover').style.display='block';
var dlg=new Dialog('dlgTest');
dlg.show();
//新增代码
//注册事件,为句柄(本地属性)传递一个事件处理函数
dlg.close_handler=function(){
//隐藏遮罩层
//把对遮罩层的具体操作放在本地实例中实现,避免干扰Dialog类型
//这时也就形成了自定义事件的雏形
document.getElementById('pageCover').style.display='none';
}
}
预览效果
在Dialog对象内部添加一个句柄(属性),当关闭按钮的click事件处理程序在调用close方法后,判断该句柄是否为函数,如果是函数,就调用执行该句柄函数。
在openDialog方法中,创建Dialog对象后为句柄赋值,传递一个隐藏遮罩层的方法,这样在关闭Dialog的时候,就隐藏了遮罩层,同时没有造成两个组件之间的耦合。
上面这个交互过程就是一个简单的自定义事件,即先绑定事件处理程序,然后在原生事件处理函数中调用,以实现触发事件的过程。DOM对象的事件,如button的click事件,也是类似原理。
设计事件触发模型
设计高级自定义事件。上面示例简单演示了如何自定义事件,远不及DOM预定义事件抽象和复杂,这种简单的事件处理有很多弊端:
- 没有共同性。如果在定义一个组件时,还需要编写一套类似的结构处理。
- 事件绑定有排斥性。只能绑定了一个close事件处理程序,绑定新的会覆盖之前绑定。
- 封装不够完善。如果用户不知道有个close_handler的句柄,就没有办法绑定该事件,只能去查源代码。
针对第一个弊端,我们可以使用继承来解决;对于第二个弊端,则可以提供一个容器(二维数组)来统一管理所有事件;针对第三个弊端,需要和第一个弊端结合,在自定义的事件管理对象中添加统一接口,用于添加、删除、触发事件。
/*
* 使用观察者模式实现事件监听
* 自定义事件类型
*/
function EventTarget(){
//初始化本地事件句柄为空
this.handlers={};
}
//扩展自定义事件类型的原型
EventTarget.prototype={
constructor:EventTarget, //修复EventTarget构造器为自身
//注册事件
//参数type表示事件类型
//参数handler表示事件处理函数
addHandler:function(type,handler){
//检测本地事件句柄中是否存在指定类型事件
if(typeof this.handlers[type]=='undefined'){
//如果没有注册指定类型事件,则初始化为空数组
this.handlers[type]=new Array();
}
//把当前事件处理函数推入到当前事件类型句柄队列的尾部
this.handlers[type].push(handler);
},
//注销事件
//参数type表示事件类型
//参数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;
}
}
}
},
//触发事件
//参数event表示事件类型
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++){
//逐一调用队列中每个事件处理函数,并把参数event传递给它
handlers[i](event);
}
}
}
}
addHandler方法用于添加事件处理程序,removeHandler方法用于移除事件处理程序,所有的事件处理程序在属性handlers中统一存储管理。调用trigger方法触发一个事件,该方法接收一个至少包含type属性的对象作为参数,触发的时候会查找handlers属性中对应type的事件处理程序。
下面就可以编写如下代码,来测试自定义事件的添加和触发过程。
//自定义事件处理函数
function onClose(event){
alert('message:'+event.message);
}
//实例化自定义事件类型
var target=new EventTarget();
//自定义一个close事件,并绑定事件处理函数为onClose
target.addHandler('close',onClose);
//创建事件对象,传递事件类型,以及额外信息
var event={
type:'close',
message:'Page Cover closed!'
};
//触发close事件
target.trigger(event);
预览效果
应用事件模型
通过上一示例,简单分解了高级自定义事件的设计过程,下面示例将利用继承机制解决第一个弊端。
下面是寄生式组合继承的核心代码,这种继承方式是目前公认的JavaScript最佳继承方式。
//原型继承扩展工具函数
//参数subType表示子类
//参数superType表示父类
function extend(subType,superType){
var prototype=Object(superType.prototype);
prototype.constructor=subType;
subType.prototype=prototype;
}
最后,显示本节完善后的自定义事件的完整代码,演示效果如图3所示。

打开

关闭
图3 优化后对话框组件应用效果
用户也可以把打开Dialog时,显示遮罩层也写成类似关闭事件的方式(test5.html)。当代码中存在多个部分,在特定时刻相互交互的情况下,自定义事件就非常有用。
如果每个对象都有其它对象的引用,那么整个代码高度耦合,对象改动会影响其它对象,维护起来就困难重重,自定义事件使对象能够解耦,功能隔绝,这样对象之间就可以实现高度聚合。