深入理解事件

事件:事件是用户或浏览器自身执行的某种动作,如 click,load 和mouseover 都是事件的名字。响应某个事件的函数叫做事件处理函数 / 事件处理程序 / 事件句柄。如果想要绑定多个函数,则需要用到事件监听器。

1. 事件绑定的几种方式

javascript 给 DOM 绑定事件处理函数总的来说有2种方式:在 html 文档中绑定、在 js 代码中绑定。下面的方式1、方式2属于在 html 中绑定事件,方式3、方式4和方式5属于在js代码中绑定事件,其中,方式4和5属于事件监听,而方式5是最推荐的做法。

1)在html文档中绑定

方式1:

<input type="button" id="btn" onclick="函数名( )">

方式2:

<input type="button" id="btn" onclick="直接写函数内容">

2)在js代码中绑定

方式3:

document.getElementById("btn").onclick=function( ){ };

说明:方式3也称为“DOM0级事件处理程序”。它无法绑定多个事件,当绑定多个事件的时候,只有最后一个会生效,其他皆被覆盖)

方式4: [object].attachEvent(“事件类型”,”处理函数”)

说明: ①方式4也称为“IE事件处理程序”。这种方法不属于w3c标准,并且仅IE8及以下支持该方法; ②事件类型要加on; ③如下图,b中声明a函数时分配了一块内存地址 ,两个dom.attachEvent('onclick',a)中的a都指向的是下面定义的a;c中只是两个函数体一样的匿名函数,分别有各自的内存地址,故认为是两个不同的函数对象

方式5:[object].addEventListener(“事件类型”,”处理函数”,”冒泡事件或捕获事件”);

说明: ①方式5也称为“DOM2级事件处理程序”。w3c正统标准,IE9及以上、Chrome、Firefox等支持该方法; ②事件类型不加on; ③第三个参数不设置的时候,默认为false即冒泡; ④ 同一个事件处理函数可以绑定2次,一次用于事件捕获,一次用于事件冒泡;如果绑定的是同一个事件处理函数,并且都是事件冒泡类型或者事件捕获类型,那么只能绑定一次; ⑤ 不同的事件处理函数可以重复绑定,这点与上面attachEvent是一样的

2. 事件处理函数的执行顺序

方式123都不能实现事件的重复绑定,所以自然也就不存在执行顺序的问题。方式4和方式5可以重复绑定特性,所以需要了解下执行顺序的问题。 结论: 对于addEventListener,如果给目标的同一个事件绑定多个处理函数,先绑定的先执行。 attachEvent则刚好相反,后绑定的先执行,这是因为采用attachEvent的是IE8-,而IE8-是不支持dom事件流模型的。

<script>
    window.onload = function(){
   var outA=document.getElementById("outA");  
        outA.addEventListener('click',function(){alert(1);},false);
        outA.addEventListener('click',function(){alert(2);},true);
        outA.addEventListener('click',function(){alert(3);},true);
        outA.addEventListener('click',function(){alert(4);},true);
    };
</script>
<body>
    <div id="outA">
    </div>
</body>

当点击outA的时候,会依次打印出1、2、3、4。这里特别需要注意:我们是同时给outA这个元素绑定了多个onclick事件处理函数,没有涉及父子元素,所以也不涉及事件冒泡和事件捕获的问题,即addEventListener的第三个参数在这种场景下,没有什么用处,直接忽略之。如果是通过事件冒泡或者是事件捕获触发outA的click事件,那么函数的执行顺序会有变化。

3. 事件捕获和事件冒泡

我们知道HTML中的元素是可以嵌套的,形成类似于树的层次关系。比如下面的代码:

<div id="outA">
    <div id="outB">
        <div id="outC"></div>
    </div>
</div>

如果点击了最内侧的outC,那么外侧的outB和outC算不算被点击了呢?很显然算,不然就没有必要区分事件冒泡和事件捕获了,这一点各个浏览器厂家也没有什么疑义。假如outA、outB、outC都注册了click类型事件处理函数,当点击outC的时候,触发顺序是A–>B–>C,还是C–>B–>A呢?如果浏览器采用的是事件冒泡,那么触发顺序是C–>B–>A,由内而外,像气泡一样,从水底浮向水面;如果采用的是事件捕获,那么触发顺序是A–>B–>C,从上到下,像石头一样,从水面落入水底。

一般来说事件冒泡机制用的更多一些,所以在IE8以及之前,IE只支持事件冒泡。IE9+/FF/Chrome这2种模型都支持,可以通过addEventListener的第三个参数来设定,false代表事件冒泡,true代表事件捕获。

<script>
    window.onload = function(){
        var outA = document.getElementById("outA");  
        var outB = document.getElementById("outB");  
        var outC = document.getElementById("outC");  

        // 使用事件冒泡
        outA.addEventListener('click',function(){alert(1);},false);
        outB.addEventListener('click',function(){alert(2);},false);
        outC.addEventListener('click',function(){alert(3);},false);
    };
</script>
<body>
<div id="outA">
    <div id="outB">
        <div id="outC"></div>
    </div>
</div>
</body>

使用的是事件冒泡,当点击outC的时候,打印顺序是3–>2–>1。如果将false改成true使用事件捕获,打印顺序是1–>2–>3。

4. DOM事件流

4.1 事件流定义:

事件流描述的是从页面中接收事件的顺序。 事件发生时会在元素节点与根节点之间按照特定的顺序如流水一样传播,路径所经过的所有节点都会收到该事件,这个传播过程即事件流。

4.2 事件流模型:

事件传播的顺序对应浏览器的两种事件流模型:捕获型事件流和冒泡型事件流。

冒泡型事件流:事件的传播是从最特定的事件目标到最不特定的事件目标。即由内到外 捕获型事件流:事件的传播是从最不特定的事件目标到最特定的事件目标。即由外到内

4.3 DOM事件流:

4.3.1 dom事件流定义:

DOM标准采用捕获+冒泡的DOM事件流。两种事件流都会触发DOM的所有对象,从document对象开始,也在document对象结束。

4.3.2 dom事件流包括:

DOM标准规定事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。 事件捕获阶段:实际目标(<div>)在捕获阶段不会接收事件。也就是在捕获阶段,事件从document到<html>再到<body>就停止了。上图中为1~3. 处于目标阶段:事件在<div>上发生并处理。但是事件处理会被看成是冒泡阶段的一部分 冒泡阶段:事件又传播回文档。

4.3.3 dom事件流与冒泡、捕获

将DOM事件流看作整个过程,那么其实 useCapture=false意味着:将该事件处理函数加入到冒泡阶段,在冒泡阶段会被调用;useCapture=true意味着:将该事件处理函数加入到捕获阶段,在捕获阶段会被调用。从DOM事件流模型可以看出,捕获阶段的事件处理函数,一定比冒泡阶段的事件处理函数先执行。

4.3.4 dom事件流的相关概念

1) target: 触发事件的某个具体对象,固定不变的。 2) currentTarget: 绑定事件的对象,恒等于this,可能出现在事件流的任意一个阶段中。动态变化的。

控制台:

点击最里面的son3后,可以看到target一直不变,而由于冒泡,导致currentTarget动态变化。

3) 两者的应用场合 通常情况下target和currentTarget是一致的,我们只要使用terget即可,但有一种情况必须区分这三者的关系,那就是在父子嵌套的关系中,父元素绑定了事件,单击了子元素(根据事件流,在不阻止事件流的前提下他会传递至父元素,导致父元素的事件处理函数执行),这时候currentTarget指向的是父元素,因为他是绑定事件的对象,而target指向了子元素,因为他是触发事件的那个具体对象

PS: 注意!!!并非所有的事件都会经过冒泡阶段 。所有的事件都要经过捕获阶段和处于目标阶段,但是有些事件会跳过冒泡阶段:如,获得输入焦点的focus事件和失去输入焦点的blur事件。

5. DOM事件流中的“事件处理函数的执行顺序”

我们回头再来说事件处理函数的执行顺序。

点击outC的时候,打印顺序是 : capture1–>capture2–>target2–>target1–>bubble2–>bubble1。 由于outC是我们触发事件的目标对象,在outC上注册的几个事件处理函数都属于DOM事件流中的目标阶段。对同一个绑定对象(这里是outC)而言,同处于目标阶段的多个函数的执行顺序:先注册的先执行,后注册的后执行。这就是上面我们说的,在目标对象(outC)上绑定的函数是采用捕获,还是采用冒泡,都没有什么关系,因为冒泡和捕获只是对父元素上的函数执行顺序有影响,对自己没有什么影响。 至此我们可以给出事件函数执行顺序的结论了:

捕获阶段的处理函数最先执行,其次是目标阶段的处理函数,最后是冒泡阶段的处理函数。目标阶段的处理函数如果有多个,则先注册的先执行,后注册的后执行。

6. 阻止事件冒泡和捕获

默认情况下,多个事件处理函数会按照DOM事件流模型中的顺序执行。如果子元素上发生某个事件,不需要执行父元素上注册的事件处理函数,那么我们可以停止捕获和冒泡,避免没有意义的函数调用。前面提到的5种事件绑定方式,都可以实现阻止事件的传播。由于第5种方式,是最推荐的做法。所以我们基于第5种方式,看看如何阻止事件的传播行为。 注意: IE8以及IE8之前可以通过 window.event.cancelBubble=true阻止事件的继续传播;IE9+/FF/Chrome通过event.stopPropagation()阻止事件的继续传播。

当点击outC的时候,打印出capture–>target,不会打印出bubble。 因为当事件传播到outC上的处理函数时,通过stopPropagation阻止了事件的继续传播,所以不会继续传播到冒泡阶段。想要在哪个节点阻止传播,就在哪个节点的事件处理函数中添加stopPropagation,记得要传参。

7. 事件代理/事件委托

7.1 概述:

事件委托又叫事件代理。JavaScript高级程序设计上讲:事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。我们可以用取快递的例子来理解这个东西。

假设:有三个同事预计会在周一收到快递。为签收快递,有两种办法:一是三个人在公司门口等快递;二是委托给前台代为签收。现实当中,我们大都采用委托的方案(公司也不会容忍那么多员工站在门口就为了等快递)。前台收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台也会在收到寄给新员工的快递后核实并代为签收。

这里其实还有2层意思的: 第一,现在委托前台的同事是可以代为签收的,即程序中的现有的dom节点是有事件的; 第二,新员工也是可以被前台代为签收的,即程序中新添加的dom节点也是有事件的。

7.2 为什么要使用事件委托:

简单来说,就是为了减少不必要的dom操作,优化性能。

一般来说,dom需要有事件处理程序,我们都会直接给它设事件处理程序就好了,那如果是很多的dom需要添加事件处理呢?比如我们有100个li,每个li都有相同的click点击事件,可能我们会用for循环的方法,来遍历所有的li,然后给它们添加事件,那这么做会存在什么影响呢?

在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,因为需要不断的与dom节点进行交互,访问dom的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少DOM操作的原因;如果要用事件委托,就会将所有的操作放到js程序里面,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能;

每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大,自然性能就越差了。比如上面的100个li,就要占用100个内存空间,如果是1000个,10000个呢?如果用事件委托,那么我们就可以只对它的父级(如果只有一个父级)这一个对象进行操作,这样我们就需要一个内存空间就够了,自然性能就会更好。

7.3 事件委托的原理:

事件委托是利用事件的冒泡原理来实现的,何为事件冒泡呢?就是事件从最深的节点开始,然后逐步向上传播事件,举个例子:页面上有这么一个节点树,div>ul>li>a;比如给最里面的a加一个click点击事件,那么这个事件就会一层一层的往外执行,执行顺序a>li>ul>div,有这样一个机制,那么我们给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托,委托它们父级代为执行事件。

7.4 事件委托如何实现:

终于到了本文的核心部分了。在介绍事件委托的方法之前,我们先来看例一: 需求:不管点击哪个li,都能弹出123:

<ul id="ul1">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
window.onload = function(){
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName('li');
for(var i=0;i<aLi.length;i++){
aLi[i].onclick = function(){
alert(123);
}
}
}

上面的代码的意思很简单,相信很多人都是这么实现的,我们看看有多少次的dom操作:首先要找到ul,然后遍历li,然后点击li的时候,又要找一次目标的li的位置,才能执行最后的操作,每次点击都要找一次li; 那么我们用事件委托的方式做又会怎么样呢?

window.onload = function(){
var oUl = document.getElementById("ul1");
oUl.onclick = function(){
alert(123);
}
}

我们让父级ul监听点击事件,则不管是哪个li被点击————由于冒泡原理,事件最终都会冒泡到ul上,触发ul上的点击事件,弹出123。当然,这里当点击ul的时候,也是会触发的。

那么问题就来了,如果我想让事件代理的效果直接给某个指定的节点的事件效果一样怎么办?比如说只有点击li才会触发,不怕,我们有绝招:

事件本身是一个对象,即Event对象,事件发生时该对象作为参数传给回调函数。而Event对象提供了一个属性叫target,可以返回事件的目标节点,我们称之为事件源.

也就是说,target可以表示为当前的事件直接操作的那个dom。当然,这个是有兼容性的,标准浏览器用ev.target,IE浏览器用event.srcElement,此时只是获取了当前节点的位置,并不知道是什么节点名称,这里我们用nodeName来获取具体是什么标签名,这个返回的是一个大写的,我们需要转成小写再做比较(习惯问题):

window.onload = function(){
  var oUl = document.getElementById("ul1");
  oUl.onclick = function(ev){
    var ev = ev || window.event;
    var target = ev.target || ev.srcElement;
    if(target.nodeName.toLowerCase() == 'li'){
         alert(123);
         alert(target.innerHTML);
    }
  }
}

这样改下就只有点击li会触发事件了,且每次只执行一次dom操作,如果li数量很多的话,将大大减少dom的操作,优化的性能可想而知! 上面的例子是说li操作的是同样的效果,要是每个li被点击的效果都不一样,那么用事件委托还有用吗?请看例二:

<div id="box">
<input type="button" id="add" value="添加" />
<input type="button" id="remove" value="删除" />
<input type="button" id="move" value="移动" />
<input type="button" id="select" value="选择" />
</div>
window.onload = function(){
var Add = document.getElementById("add");
var Remove = document.getElementById("remove");
var Move = document.getElementById("move");
var Select = document.getElementById("select");
Add.onclick = function(){
alert('添加');
};
Remove.onclick = function(){
alert('删除');
};
Move.onclick = function(){
alert('移动');
};
Select.onclick = function(){
alert('选择');
}
}

上面实现的效果很简单,4个按钮对应4个不同的操作,那么至少需要4次dom操作,如果用事件委托,能进行优化吗?

window.onload = function(){
var oBox = document.getElementById("box");
oBox.onclick = function (ev) {
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLocaleLowerCase() == 'input'){
switch(target.id){
case 'add' :
alert('添加');
break;
case 'remove' :
alert('删除');
break;
case 'move' :
alert('移动');
break;
case 'select' :
alert('选择');
break;
}
}
}
}

可见,用事件委托就可以只用一次dom操作就能完成所有的效果,比上面的性能肯定是要好一些的

现在讲的都是document加载完成的现有dom节点下的操作,那么如果是新增的节点,新增的节点会有事件吗?也就是说,一个新员工来了,他能收到快递吗?

请看例三: 我们的需求是是: ①移入li,li变红,移出li,li变白 ②对于点击按钮后新增的li节点,仍然具有该效果。

以下是正常的方法:

<input type="button" name="" id="btn" value="添加" />
<ul id="ul1">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
window.onload = function(){
var oBtn = document.getElementById("btn");
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName('li');
var num = 4;
//鼠标移入变红,移出变白
for(var i=0; i<aLi.length;i++){
aLi[i].onmouseover = function(){
this.style.background = 'red';
};
aLi[i].onmouseout = function(){
this.style.background = '#fff';
}
}
//添加新节点
oBtn.onclick = function(){
num++;
var oLi = document.createElement('li');
oLi.innerHTML = 111*num;
oUl.appendChild(oLi);
};
}

你会发现,新增的li是没有事件的,说明添加子节点的时候,事件没有一起添加进去——这是因为li遍历这一动作发生在新增li这一动作之前,在那个时候已经确定了li的个数是4,因此只绑定了4个li。这不是我们想要的结果,那怎么做呢?一般的解决方案会是这样,将for循环封装在一个函数里,命名为mHover,如下:

window.onload = function(){
var oBtn = document.getElementById("btn");
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName('li');
var num = 4;
function mHover () {
//鼠标移入变红,移出变白
for(var i=0; i<aLi.length;i++){
aLi[i].onmouseover = function(){
this.style.background = 'red';
};
aLi[i].onmouseout = function(){
this.style.background = '#fff';
}
}
}
mHover ();
//添加新节点
oBtn.onclick = function(){
num++;
var oLi = document.createElement('li');
oLi.innerHTML = 111*num;
oUl.appendChild(oLi);
mHover ();
};
}

与上面方法不同的是,这个方法没有在新增li之前就为原有li绑定事件,而是在新增li后遍历所有的li(包括新增li),并一起绑定事件。虽然功能实现了,看着还挺好,但实际上无疑又增加了一个dom操作,在优化性能方面是不可取的,那么用事件委托的方式,能做到优化吗?

window.onload = function(){
var oBtn = document.getElementById("btn");
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName('li');
var num = 4;
//事件委托,添加的子元素也有事件
oUl.onmouseover = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLowerCase() == 'li'){
target.style.background = "red";
}
};
oUl.onmouseout = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLowerCase() == 'li'){
target.style.background = "#fff";
}
};
//添加新节点
oBtn.onclick = function(){
num++;
var oLi = document.createElement('li');
oLi.innerHTML = 111*num;
oUl.appendChild(oLi);
};
}

如上,新添加的节点是带有事件效果的。根据事件冒泡原理,不管是原有li还是新增li,只要鼠标一移入li中就等同于鼠标移入ul中,自然会触发ul的鼠标移入事件,之后我们只要在ul的事件函数中定义相关行为就可以了。 我们可以发现,当用事件委托的时候,根本就不需要去遍历元素的子节点,只需要给父级元素添加事件就好了。这样可以大大的减少dom操作,这才是事件委托的精髓所在。

7.5 总结:

那什么样的事件可以用事件委托,什么样的事件不可以用呢? 适合用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress。 值得注意的是,mouseover和mouseout虽然也有事件冒泡,但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易。 不适合的就有很多了,举个例子,mousemove,每次都要计算它的位置,非常不好把控,至于focus,blur之类的就更不用说了,本身就没有冒泡的特性,自然就不能用事件委托了。

8 事件对象

8.1 认识事件对象

事件在浏览器中是以对象的形式存在的,即event对象。触发一个事件,就会产生一个event对象,该对象包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。 例如:鼠标操作产生的event中会包含鼠标位置的信息;键盘操作产生的event中会包含与按下的键有关的信息。 所有浏览器都支持event对象,但支持方式不同,在DOM中event对象必须作为唯一的参数传给事件处理函数,在IE中event是window对象的一个属性。

8.2 html事件处理程序中的事件对象

<input id="btn" type="button" value="click"
onclick=" console.log('html事件处理程序'+event.type)"/>

这样会创建一个包含局部变量event的函数。可通过event直接访问事件对象。

8.3 DOM中的事件对象

DOM0级和DOM2级事件处理程序都会把event作为参数传入。 参数命名:随便。习惯上用e,或者ev或者event。

<body>
<input id="btn" type="button" value="click"/>
<script>
    var btn=document.getElementById("btn");
    btn.onclick=function(event){
        console.log("DOM0 & click");
        console.log(event.type);    //click    }

    btn.addEventListener("click", function (event) {
        console.log("DOM2 & click");
        console.log(event.type);    //click    },false);
</script>
</body>

DOM中事件对象的重要属性和方法。 属性:

  • type属性,用于获取事件类型
  • target属性,用于获取事件直接作用的目标(更具体target.nodeName)
  • currentTarget属性,用于获取事件实际绑定的目标

方法:

  • stopPropagation()方法,用于阻止事件冒泡
  • preventDefault()方法,阻止事件的默认行为 移动端用的多

8.4 IE中的事件对象

第一种情况: 通过DOM0级方法添加事件处理程序时,event对象作为window对象的一个属性存在。

<body>
<input id="btn" type="button" value="click"/>
<script>
var btn=document.getElementById("btn");
btn.onclick= function () {
var event=window.event;
console.log(event.type); //click
}
</script>
</body>

第二种情况:通过attachEvent()添加的事件处理程序,event对象作为参数传入。

<body>
<input id="btn" type="button" value="click"/>
<script>
var btn=document.getElementById("btn");
btn.attachEvent("onclick", function (type) {
console.log(event.type); //click
})
</script>
</body>

IE中事件对象的重要属性和方法: 属性:

  • type属性,用于获取事件类型(一样)
  • srcElement属性,用于获取事件直接作用的目标(更具体srcElement.nodeName)
  • cancelBubble属性,用于阻止事件冒泡。IE中cancelBubble为属性而不是方法,true表示阻止冒泡。
  • returnValue属性,阻止事件的默认行为。false表示阻止事件的默认行为

PS:targrt和srcElement的兼容性处理如下

//兼容性处理
function showMsg(event){
event=event||window.event;
//IE8以前必须通过window获取event,DOM中就是个简单的传参
var ele=event.target || event.srcElement;
//获取目标元素,DOM中用target,IE中用srcElement
alert(ele);
}

参考链接https://blog.csdn.net/aitangyong/article/details/43231111 http://www.cnblogs.com/starof/p/4066381.html https://www.cnblogs.com/liugang-vip/p/5616484.html http://www.cnblogs.com/starof/p/4077532.html

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券