深刻理解事件机制的实现

1、一个实例

假设你在你家客厅里玩游戏,口渴了,须要到厨房开一壶水,等水开了的时候,为了防止水熬干,你须要及时把火炉关掉。为了及时了解到水是否烧开,你有三种策略能够选择:编程

1. 守在厨房内,等水烧开设计模式

这种策略显然是很愚蠢的,采起这种策略,在烧水的过程当中你将不能作任何事情,效率极低。数组

2. 呆在客厅玩游戏,每隔一两分钟跑到厨房看一次浏览器

这种策略,在计算机科学中称为轮询,即每隔必定的时间,监测一次。在这里,也是很不明智的,在玩游戏时须要不断的分心。函数

3. 在水壶上安装一个报警器,当水开了的时候,发出警报优化

这种策略是最好的,既不耽误本身玩游戏,又能在水开了的时候使本身及时得到通知。这种策略在计算机中经过事件机制来实现。编码

2、事件机制的组成

经过上面的实例,咱们能够抽象出一个事件机制有三个组成部分:spa

1.事件源:即事件的发送者,在上例中为水壶;翻译

2.事件:事件源发出的一种信息或状态,好比上例的警报声,它表明着水开了;设计

3.事件侦听者:对事件做出反应的对象,好比上例中的你。在设计事件机制时通常把侦听者设计为一个函数,当事件发送时,调用此函数。好比上例中能够把倒水设计为侦听者。

3、初步实现

可使用面向对象设计中的组合模式,把事件侦听者当作事件源内部的一个对象,当事件发生时,调用侦听者便可:

1 事件源:水壶{
2     事件侦听者:你关火;//事件源持有事件侦听者
3 
4     发送(事件:“水开了”){
5         你关火();
6     }
7 }

4、出现多个事件侦听者的状况

若是你和你女友都在客厅玩游戏,水开的时候应该谁去关火呢?假设精明(懒惰)的你,听到水开的警报声,立刻伪装上厕所,你女友只能无奈地去关火。这种状况下,水壶发出的警报声致使了两个反应:1.你上厕所,2.你女友去关火。此时咱们要如何实现呢?咱们依然能够采用上面的实现方案,再在事件源中添加一个事件侦听者:

 1 事件源:水壶{
 2     事件侦听者:你上厕所;
 3     事件侦听者:你女友关火;
 4 
 5     发送(事件:“水开了”){
 6             你上厕所();
 7             你女友关火();
 8     }
 9 
10 }

但这种设计有一个重大缺陷:事件源和事件侦听者过分耦合。全部侦听者都是硬编码入事件源中,在程序执行过程当中没法更改,灵活性极差。好比,有一天你女友外出了,只能你去关火,那么上面的事件源就须要从新修改。咱们能够采用下面的方法使事件源和事件侦听者解耦:

1.事件源中定义一个列表,好比数组,用来存储全部侦听者;

2.为列表留一个增删数据的接口,用来随时添加和删除侦听者;

3.当发送事件时,遍历并执行列表中的侦听者

实现以下:

 1 事件源:水壶{
 2 
 3     事件侦听者:侦听者列表[];
 4 
 5     添加事件侦听者(侦听者){
 6         侦听者列表加入侦听者
 7     }
 8     删除事件侦听者(侦听者){
 9         侦听者列表移除侦听者
10     }
11 
12     发送(事件){
13       //遍历并执行列表中的侦听者
14       for(侦听者 in 侦听者列表){
15           执行侦听者
16       }
17   }
18 
19 }

这种实现方案即为观察者设计模式,可让侦听者预订事件。

5、事件源可发送多种事件的状况

假设你家的水壶有点智能,当水温达到90度的时候,会发出一个“水快开了”的警报,为你提早逃到厕所偷懒留出了充足的时间,这种状况下的事件和侦听者的对应关系以下:

咱们能够在添加和删除侦听者的时候,把事件类型和侦听者绑定成一个数组(或对象),再加入侦听者列表。在发送事件时,在列表中查找和当前事件绑定的侦听器执行:

事件源:水壶{

    事件侦听者:侦听者列表[];

    添加事件侦听者(事件类型,侦听者){
        带类型侦听者=[事件类型,侦听者];//经过数组把事件类型和侦听者绑定
        侦听者列表加入带类型侦听者;
    }
    删除事件侦听者(事件类型,侦听者){
        经过事件类型和侦听者查找列表中对应的侦听器删除;
    }

    发送(事件类型){
        //遍历并执行列表中的侦听者
        for(带类型侦听者 in 侦听者列表){
            if(带类型侦听者[0]==事件类型){
                    带类型侦听者[1]()//执行对应的侦听器    
            }
        }
    }
}

把上面的文字描述翻译成伪码以下:

 1 //水壶类
 2 Kettle{
 3 
 4     array:Listeners[];
 5 
 6     addEventListener(eventType,listener){
 7         typeListener=[eventType,listener];//经过数组把事件类型和侦听者绑定
 8         Listeners.push(typeListener);
 9     }
10 
11     removeEventListener(eventType,listener){
12         Listeners.delete([eventType,listener]);
13     }
14 
15     dispatch(eventType){
16         //遍历并执行列表中的侦听者
17         for(typeListener in Listeners){
18             if(typeListener[0]==eventType){
19                 typeListener[1]()//执行对应的侦听器    
20             }
21         }
22     }
23 
24 }
25 
26 goWc(){
27     //你上厕所
28 }
29 
30 turnOffFire(){
31     //女友关火
32 }
33 
34 kettle=new Kettle();
35 //水壶注册水快开了事件
36 kettle.addEventListener("水快开了",goWC);
37 kettle.addEventListener("水开了",turnOffFire);    
38 kettle.dispatch("水快开了");

优化:遵循"针对接口编程"的设计原则,应该为水壶、事件、侦听器设计一个基类,其余具体的类继承这些基类;

6、显示对象上的事件:理解事件流

当事件发生在显示对象上(好比浏览器)的时候,会遇到一个颇有趣的问题:页面的那一部分会拥有某个特定的事件?好比当你点击页面上的一栋小房子的时候,根据视角的远近,你点击的对象会发生变化。从最远处来看你点击的是页面,镜头拉近你点击的是小房子,再拉近你点击的是房子上的一面墙,再拉近你点击的是墙上的一块砖。也就是说,你点击一次页面也许会有不少显示对象发生了点击事件,若是你在每个显示对象上都绑定了点击处理程序,那么这些程序都会执行。这里会遇到一个问题:这些程序按什么顺序执行。这取决于显示对象接受到点击事件的顺序,通常有两种模式:事件冒泡和事件捕获。这种事件在显示对象上按顺序发生的过程称为事件流。

1. 事件冒泡

事件冒泡,即事件开始时由最具体的元素(好比上例的砖块)接受,而后逐级向上传播到较为不具体的节点(文档);

2. 事件捕获

事件捕获的思想是不太具体的元素(文档)更早的接受事件,而最具体的元素最后接受到事件(砖块)。事件捕获的用意在于事件到达预订目标之间捕获它。

 

在JavaScript中为DOM中的元素添加事件处理程序时,有三个参数,其中第三个参数是一个布尔值,当为true时,表示在捕获阶段调用事件处理程序,为false时,表示在冒泡阶段调用事件处理程序,举例以下:

 1 <body>
 2     <div id="outer">
 3         <div id="inner" >
 4         </div>
 5     </div>
 6 </body>
 7 
 8 //例一
 9 var btn1=document.getElementById("outer");
10 btn1.addEventListener("click",function(){
11     alert('outer')
12 },false);
13 
14 var btn2=document.getElementById("inner");
15 btn2.addEventListener("click",function(){
16     alert('inner')
17 },false);
18 
19 //例二
20 var btn1=document.getElementById("outer");
21 btn1.addEventListener("click",function(){
22     alert('outer')
23 },false);
24 
25 var btn2=document.getElementById("inner");
26 btn2.addEventListener("click",function(){
27     alert('inner')
28 },false);    

上面例一的事件处理程序都发生在冒泡阶段,因此会先输出inner,再输出outer。例二中id为outer元素上的事件处理程序发生在捕获阶段,因此会先输出outer,再输出inner。

注意:事件流发生在父元素和子元素之间,而不是两个同级的元素。

相关文章
相关标签/搜索