事件机制

无论是前端还是移动端,用户在浏览网页或者APP时,通常会在屏幕上产生很多交互操作,例如点击、选择、滚动屏幕、键盘输入等待,并且网页或APP也会根据不同的操作进行响应变化。这种基于事件的处理方式,本质上是一种消息传递机制,称之为事件机制。

在事件机制中,有3样最重要的东西:

  • 事件生产者
  • 事件对象
  • 事件消费者

事件生产者可以产生一系列的事件对象,然后事件对象携带着必要的信息,传递给事件消费者。

上图所示是一种单向的消息传递模型,事件消息总是由事件生产者传递给事件消费者。而如果要使得事件生产者和事件消费者形成双向通信,那么很简单,让两者同时作为事件生产者和事件消费者就可以了。

然后呢,一个事件可以传递给多个接受对象,即一个事件生产者产生的事件可以对应有多个事件消费者:

相反地,一个事件消费者也可以接受多个事件生产者产生的事件消息:

一、事件流及事件绑定

EMCAScript标准规定事件流包含三个阶段,分别为事件捕获阶段,处于目标阶段,事件冒泡阶段。

<html>
    <body>
        <div>
            <button id="mybtn" onclick="buttonClickHandler(event)">点我试试</button>
        </div>
    </body>
</html>
<script>
    function buttonClickHandler(event) {
        console.log('button clicked')
    }
</script>

在上面的代码中,如果点击按钮button,则标准事件触发分别经历以下三个阶段:

在W3C模型中,任何事件发生时,先从顶层开始进行事件捕获,直到事件触发到达了事件源元素,这个过程叫做事件捕获(这其实也是事件的传递过程);然后,该事件会随着DOM树的层级路径,由子节点向父节点进行层层传递,直至到达document,这个过程叫做事件冒泡(也可以说这是事件的响应过程)。虽然大部分的浏览器都遵循着标准,但是在IE浏览器中,事件流却是非标准的。而IE中事件流只有两个阶段:处于目标阶段,冒泡阶段。 

下面看一个Chrome浏览器中的例子:

<html>
<head>
<style>
ul{
    background : gray;
    padding : 20px;
}
ul li{
    background : green;
}
</style>
</head>
<body>
<ul>
    <li>点我试试</li>
</ul>
<script>
var ul = document.getElementsByTagName('ul')[0];
var li = document.getElementsByTagName('li')[0];
document.addEventListener('click',function(e){console.log('document clicked')},true);//第三个参数为true使用捕获
ul.addEventListener('click',function(e){console.log('ul clicked')},true);
li.addEventListener('click',function(e){console.log('li clicked')},true);
</script>
</body>
</html>

以上代码中,我们创建了一个列表项,点击“点我试试”,看看会有什么情况发生:

document clicked
ul clicked
li clicked

在我们的开发者工具控制台上,可以看到打印出了这样三行结果,这是我们预料之中的事情,因为在这里事件捕获起了作用,点击事件依次触发了document、ul节点、li节点。

而在IE中只支持冒泡机制,所以只能在冒泡阶段进行事件绑定以及事件撤销:

target.attachEvent(type, listener);  //target: 文档节点、document、window 或 XMLHttpRequest。
                                     //函数参数: type:注册事件类型;
                                     //         listener:事件触发时的回调函数。
target.detachEvent(type,listener);   //参数与注册参数相对应。

下面看一个IE浏览器里的例子:

<html>
<body>
<ul>
    <li>点我试试</li>
</ul>
<script>
var ul = document.getElementsByTagName('ul')[0];
var li = document.getElementsByTagName('li')[0];
document.attachEvent('onclick',function(event){console.log('document clicked')})
ul.attachEvent('onclick',function(event){console.log('ul clicked')});
li.attachEvent('onclick',function(event){console.log('li clicked')});
</script>
</body>
</html>

同样地,我们点击“点我试试”,开发者工具控制台里打印出了下面的结果:

li clicked
ul clicked
document clicked

然而有时候事件的捕获机制以及冒泡机制也会带来副作用,比如冒泡机制会触发父节点上原本并不希望被触发的监听函数,所以有办法可以使得冒泡提前结束吗?我们只需要在希望事件停止冒泡的位置,调用event对象的stopPropagation函数(IE浏览器中为cancelBubble)即可终止事件冒泡了。比如在上面IE浏览器中示例代码作如下修改:

li.attachEvent('onclick',function(event){
    console.log('li clicked');
    event.cancelBubble=true;
});

修改后,再次点击“点我试试”,在控制台里只打印出一行结果,ul节点和document不会再接收到冒泡上来的click事件,因而它们注册的事件处理函数也将不会被触发了:

li clicked

二、事件委托

什么是事件委托呢
事件委托就是利用事件冒泡机制,指定一个事件处理程序,来管理某一类型的所有事件。这个事件委托的定义不够简单明了,可能有些人还是无法明白事件委托到底是啥玩意。查了网上很多大牛在讲解事件委托的时候都用到了取快递这个例子来解释事件委托,不过想想这个例子真的是相当恰当和形象的,所以就直接拿这个例子来解释一下事件委托到底是什么意思:
公司的员工们经常会收到快递。为了方便签收快递,有两种办法:一种是快递到了之后收件人各自去拿快递;另一种是委托前台MM代为签收,前台MM收到快递后会按照要求进行签收。很显然,第二种方案更为方便高效,同时这种方案还有一种优势,那就是即使有新员工入职,前台的MM都可以代替新员工签收快递。
这个例子之所以非常恰当形象,是因为这个例子包含了委托的两层意思:
首先,现在公司里的员工可以委托前台MM代为签收快递,即程序中现有的dom节点是有事件的并可以进行事件委托;其次,新入职的新员工也可以让前台MM代为签收快递,即程序中新添加的dom节点也是有事件的,并且也能委托处理事件。

为什么要用事件委托呢
当dom需要处理事件时,我们可以直接给dom添加事件处理程序,那么当许多dom都需要处理事件呢?比如一个ul中有100li,每个li都需要处理click事件,那我们可以遍历所有li,给它们添加事件处理程序,但是这样做会有什么影响呢?我们知道添加到页面上的事件处理程序的数量将直接影响到页面的整体运行性能,因为这需要不停地与dom节点进行交互,访问dom的次数越多,引起浏览器重绘和重排的次数就越多,自然会延长页面的交互就绪时间,这也是为什么可以减少dom操作来优化页面的运行性能;而如果使用委托,我们可以将事件的操作统一放在js代码里,这样与dom的操作就可以减少到一次,大大减少与dom节点的交互次数提高性能。同时,将事件的操作进行统一管理也能节约内存,因为每个js函数都是一个对象,自然就会占用内存,给dom节点添加的事件处理程序越多,对象越多,占用的内存也就越多;而使用委托,我们就可以只在dom节点的父级添加事件处理程序,那么自然也就节省了很多内存,性能也更好。
事件委托怎么实现呢?因为冒泡机制,既然点击子元素时,也会触发父元素的点击事件。那么我们就可以把点击子元素的事件要做的事情,交给最外层的父元素来做,让事件冒泡到最外层的dom节点上触发事件处理程序,这就是事件委托。
在介绍事件委托的方法之前,我们先来看看处理事件的一般方法:

<ul id="list">
    <li id="item1" >item1</li>
    <li id="item2" >item2</li>
    <li id="item3" >item3</li>
</ul>

<script>
var item1 = document.getElementById("item1");
var item2 = document.getElementById("item2");
var item3 = document.getElementById("item3");

item1.onclick = function(event){
    alert(event.target.nodeName);
    console.log("hello item1");
}
item2.onclick = function(event){
    alert(event.target.nodeName);
    console.log("hello item2");
}
item3.onclick = function(event){
    alert(event.target.nodeName);
    console.log("hello item3");
}
</script>

上面的代码意思很简单,就是给列表中每个li节点绑定点击事件,点击li的时候,需要找一次目标li的位置,执行事件处理函数。

那么我们用事件委托的方式会怎么做呢(查看示例)?

<ul id="list">
    <li id="item1" >item1</li>
    <li id="item2" >item2</li>
    <li id="item3" >item3</li>
</ul>

<script>
var item1 = document.getElementById("item1");
var item2 = document.getElementById("item2");
var item3 = document.getElementById("item3");
var list = document.getElementById("list");
list.addEventListener("click",function(event){
 var target = event.target;
 if(target == item1){
    alert(event.target.nodeName);
    console.log("hello item1");
 }else if(target == item2){
    alert(event.target.nodeName);
    console.log("hello item2");
 }else if(target == item3){
    alert(event.target.nodeName);
    console.log("hello item3");
 }
});
</script>

我们为父节点添加一个click事件,当子节点被点击的时候,click事件会从子节点开始向上冒泡。父节点捕获到事件之后,通过判断event.target来判断是否为我们需要处理的节点, 从而可以获取到相应的信息,并作处理。很显然,使用事件委托的方法可以极大地降低代码的复杂度,同时减小出错的可能性。

我们再来看看当我们动态地添加dom时,使用事件委托会带来哪些优势?首先我们看看正常写法:

<ul id="list">
    <li id="item1" >item1</li>
    <li id="item2" >item2</li>
    <li id="item3" >item3</li>
</ul>

<script>
var list = document.getElementById("list");

var item = list.getElementsByTagName("li");
for(var i=0;i<item.length;i++){
    (function(i){
        item[i].onclick = function(){
            alert(item[i].innerHTML);
        }
    })(i);
}

var node=document.createElement("li");
var textnode=document.createTextNode("item4");
node.appendChild(textnode);
list.appendChild(node);

</script>

点击item1到item3都有事件响应,但是点击item4时,没有事件响应。说明传统的事件绑定无法对动态添加的元素而动态的添加事件。

而如果使用事件委托的方法又会怎样呢(查看示例)?

<ul id="list">
    <li id="item1" >item1</li>
    <li id="item2" >item2</li>
    <li id="item3" >item3</li>
</ul>

<script>
var list = document.getElementById("list");

document.addEventListener("click",function(event){
    var target = event.target;
    if(target.nodeName == "LI"){
        alert(target.innerHTML);
    }
});

var node=document.createElement("li");
var textnode=document.createTextNode("item4");
node.appendChild(textnode);
list.appendChild(node);

</script>

当点击item4时,item4有事件响应,这说明事件委托可以为新添加的DOM元素动态地添加事件。我们可以发现,当用事件委托的时候,根本就不需要去遍历元素的子节点,只需要给父级元素添加事件就好了,其他的都是在js里面的执行,这样可以大大地减少dom操作,这就是事件委托的精髓所在。