自定义事件

课后整理 2020-12-14

设计弹出对话框

无论从事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">&times;</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预定义事件抽象和复杂,这种简单的事件处理有很多弊端:

针对第一个弊端,我们可以使用继承来解决;对于第二个弊端,则可以提供一个容器(二维数组)来统一管理所有事件;针对第三个弊端,需要和第一个弊端结合,在自定义的事件管理对象中添加统一接口,用于添加、删除、触发事件。

/* 
* 使用观察者模式实现事件监听 
* 自定义事件类型 
*/
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)。当代码中存在多个部分,在特定时刻相互交互的情况下,自定义事件就非常有用。

如果每个对象都有其它对象的引用,那么整个代码高度耦合,对象改动会影响其它对象,维护起来就困难重重,自定义事件使对象能够解耦,功能隔绝,这样对象之间就可以实现高度聚合。