JavaScript事件详解

JavaScript与HTML之间的交互是通过事件来实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。可以用侦听器来预订事件,以便事件发生的时候执行相应的代码。

 

事件流

事件流描述了从页面中接收事件的顺序,包括事件冒泡和事件捕获。

事件冒泡

事件最开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。

譬如有如下嵌套的HTML页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Event</title>
</head>
<body>
    <div>
        <p>点击</p>
    </div>
</body>
</html>

如果点击p元素,那么click事件首先在p元素上发生,这个元素是我们单击的元素。然后,click事件沿着DOM树向上传播,在每一级节点上都会发生,直到传播到document对象。传播顺序如下:

p -> div -> body -> html -> document

事件捕获

事件捕获的思想是不太具体的节点应该更早接收事件,最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前捕获它。

由于老版本浏览器不支持,因此很少有人使用事件捕获。

DOM事件流

“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段
document -> html -> body -> div -> p-> div -> body -> html -> document

IE8及更早版本不支持DOM事件流

 

事件处理程序

响应事件的函数叫做事件处理程序或事件侦听器,我们可以通过如下方式为事件指定事件处理程序。

HTML事件处理程序

某个元素支持的每种事件都可以使用一个与相应事件处理程序同名的HTML特性来指定。这个特性的值应该是能够执行的JavaScript代码。

<input type="button" value="click me" onclick="alert('clicked')">

这样指定事件处理程序具有一些独到之处。首先,这样会创建一个封装着元素属性值的函数。这个函数中有一个局部变量event,也就是事件对象。

<!-- 输出 'click' -->
<input type="button" value="click me" onclick="alert(event.type)">

通过event变量,可以直接访问事件对象,不需要自己定义或者从函数的参数列表中读取。

在这个函数内部,this指向事件的目标元素,例如:

<!-- 输出 click me-->
<input type="button" value="click me" onclick="alert(this.value)">

关于这个动态创建的函数,另一个有意思的地方是它扩展作用域的方式。在这个函数内部,可以像访问局部变量一样访问document以及该元素本身的成员。这个函数使用with想下面这样扩展作用域:

function() {
    with(document) {
        with(this) {
            //元素属性
        }
    }
}

这样一来,我们就可以更简单的访问自己的属性,如下和前面的例子效果相同。

<!-- 输出 click me-->
<input type="button" value="click me" onclick="alert(value)">

如果当前元素是个表单输入元素,则表用域中还会包含访问表单元素(父元素)的入口,这个函数就变成了如下所示:

function() {
    with(document) {
        with(this.form) {
            with(this) {
                //元素属性
            }
        }
    }
}
<!-- username中的值 -->
<form action="bg.php">
    <input type="text" name="username">
    <input type="password" name="password">
    <input type="button" value="Click Me" onclick="alert(username.value)">
</form>

使用HTML事件处理程序的缺点

时差问题:用户可能会在HTML元素一出现在页面上就触发相应的事件,但是当时事件处理程序可能不具备执行条件。譬如:

<input type="button" value="click me" onclick="clickFun();">

假设clickFun函数是在页面最底部定义的,那么在页面解析该函数之前点击都会引发错误。因此,很多HTML事件处理程序都会被封装到try-catch之中:

<input type="button" value="click me" onclick="try{clickFun();}catch(ex){}">

浏览器兼容问题:这样扩展事件处理程序的作用域链在不同浏览器中会导致不同的结果。不同JavaScript引擎遵循的标识符解析规则略有差异,很可能会在访问非限定对象成员时出错。

代码耦合:HTML事件处理程序会导致HTML代码和JavaScript代码紧密耦合。如果要更改事件处理成程序需要同时修改HTML代码和JavaScript代码。

 

DOM0级事件处理程序

通过JavaScript指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。这样的优势一是简单,二是浏览器兼容性好。

var btn = document.getElementById('btn');
btn.onclick = function() {
    alert('clicked');
}

通过DOM0级方式指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行;换句话说,程序中的this引用当前元素:

var btn = document.getElementById('btn');
btn.onclick = function() {
    alert(this.id); //输出 'btn'
}

我们可以在事件处理程序中通过this访问元素的任何属性和方法。以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。

也可以删除通过DOM0级方法指定的事件处理程序:

btn.onclick = null;

如果我们使用HTML指定事件处理程序,那么onclick属性的值就是一个包含着在同名HTML特性中指定的代码的函数。

<input id="btn" type="button" value="click me" onclick="alert(123);">
<script>
    var btn = document.getElementById('btn');
    //输出function onclick(event) {  alert(123);} 
    alert(btn.onclick); 
    //单击按钮没反应
    btn.onclick = null;
</script>

 

DOM2级事件处理程序

“DOM2级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener和removeEventListener。所有DOM节点中都包含这两个方法,并且都接收3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。如果这个布尔值参数为true,表示在捕获阶段调用事件处理函数;如果是false,表示在冒泡阶段调用事件处理函数。

var btn = document.getElementById('btn');
btn.addEventListener('click',function() {
    alert(this.id);
},false);

与DOM0级方法一样,添加的事件处理程序也是在其依附的元素的作用域中运行 ,另外,通过这种方式可以添加多个事件处理程序,添加的事件处理程序会按照添加它们的顺序出发。

var btn = document.getElementById('btn');
btn.addEventListener('click',function() {
    alert(this.id);
},false);
btn.addEventListener('click',function() {
    alert(this.type);
},false);

问题

我们给一个dom同时绑定两个点击事件,一个用捕获,一个用冒泡,那么事件的执行顺序是怎么样的?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Event</title>
    <style>
        div {
            padding: 30px;
            border: 1px solid #000;
        }
    </style>
</head>
<body>
    <div id="one">
        <div id="two">
            <div id="three">
                <div id="four">Click Me</div>
            </div>
        </div>
    </div>
    <script>
        window.onload = function() {
            one.addEventListener('click',function(){
                alert('one');
            },true);
            two.addEventListener('click',function(){
                alert('two,bubble');
            },false);
            two.addEventListener('click',function(){
                alert('two,capture');
            },true);
            three.addEventListener('click',function(){
                alert('three,capture');
            },true);
            four.addEventListener('click',function(){
                alert('four');
            },true);
        }
    </script>
</body>
</html>

点击two,执行结果:one   two,bubble   two,capture

点击three,执行结果:one   two,capture   three,capture   two,bubble

分析:

绑定在被点击元素的事件是按照代码顺序发生,其他元素通过冒泡或者捕获“感知”的事件,按照W3C的标准,先发生捕获事件,后发生冒泡事件。所有事件的顺序是:其他元素捕获阶段事件 -> 本元素代码顺序事件 -> 其他元素冒泡阶段事件 。

通过addEventListener添加的事件处理程序只能用removeEventListener来移除;移除时传入的参数与添加处理程序时使用的参数相同,这也就意味着通过addEventListener添加的匿名函数无法移除。

var btn = document.getElementById('btn');

btn.addEventListener('click',function() {
    alert(this.id);
},false);
btn.addEventListener('click',function() {
    alert(this.type);
},false);
//不能移除
btn.removeEventListener('click',function() {
    alert(this.type);
},false)

 大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器。最好只在需要在事件到达目标之前截获它的时候将事件处理程序添加到捕获阶段。

 IE9+、Firefox、Safari、Chrome、Opera支持DOM2级事件处理程序。

 

IE事件处理程序

IE实现了类似的两个方法:attachEvent和detachEvent。这两个方法接收两个参数:事件处理程序名称和事件处理程序函数。由于IE8及更早版本只支持事件冒泡,所以通过attachEvent添加的事件处理程序都会被添加到冒泡阶段。

var btn = document.getElementById('btn');
btn.attachEvent('onclick',function() {
    alert('clicked');
})

注意第一个参数是onclick而不是click。

使用attachEvent与使用DOM0级方法的主要区别在于事件处理程序的作用域,使用attachEvent时,事件处理程序会在全局作用域中运行,因此this等于window。

var btn = document.getElementById('btn');
btn.attachEvent('onclick',function() {
    alert(this === window);  //true
})

利用attachEvent也可以为一个元素添加多个事件处理程序,但是这些事件处理程序并不是以添加它们的顺序执行,而是以相反的顺序被执行。

使用attachEvent添加的事件可以通过detachEvent来移除,条件是必须提供相同的参数,所以匿名函数将不能被移除。

支持IE事件处理程序的浏览器有IE和Opera,IE11开始将不再支持attachEvent和detachEvent。

跨浏览器的事件处理程序

function addEvent(element, type, handler) {
    if (element.addEventListener) {
        //事件类型、需要执行的函数、是否捕捉(false表示冒泡)
        //IE9+支持addEventListener,IE8及以下不支持addEventListener
        element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
        //IE11之后不再支持attachEvent
        //attachEvent添加的时间函数中this指向window
        //IE6-8只支持事件冒泡不支持事件捕获
        element.attachEvent('on' + type, handler);
    } else {
        element['on' + type] = handler;
    }
}

// 移除事件
function removeEvent(element, type, handler) {
    if (element.removeEventListener) {
        element.removeEventListener(type, handler, false);
    } else if (element.datachEvent) {
        element.detachEvent('on' + type, handler);
    } else {
        element['on' + type] = null;
    }
}

 

事件对象

在触发DOM上的某个事件时,会产生一个事件对象event,这个对象中包含着所有与事件有关的信息。

DOM中的事件对象

兼容DOM的浏览器会将一个event对象传入到事件处理程序中

var btn = document.getElementById('btn');
btn.onclick = function(event) {
    alert(event.type);
}
btn.addEventListener('click',function(event) {
    alert(event.type);
},false);
<input id="btn" type="button" value="click me" onclick="alert(event.type)">

常用属性和方法

属性方法      类型  读/写  说明

cancelable     Boolean 只读  表明是否可以取消事件的默认行为

currentTarget  Element 只读  其事件处理程序当前正在处理事件的那个元素、

eventPhase    Integer  只读  调用事件处理程序的阶段:1-捕获阶段,2-处于目标,3-冒泡阶段

preventDefault   Function  只读  取消事件默认行为,如果cancelable是true则可以使用这个方法

stopPropagation   Function 只读  取消事件的进一步捕获或者冒泡,同时阻止任何事件处理程序被调用(DOM3级事件中新增)

target      Element 只读  事件的目标

type       String  只读  被触发的事件的类型

在事件处理程序内部,this始终等于currentTarget的值,而target则只包含事件的实际目标

如果直接将事件处理程序指定给了目标元素,则this、currentTarget和target包含相同的值。

如果需要通过一个函数处理多个事件时,可以使用type属性:

var btn = document.getElementById('btn');
var handler = function(event) {
    switch(event.type) {
        case 'click':
            alert('click');
            break;
        case 'mouseover':
            alert('mouseover');
            break;
        case 'mouseout':
            alert('mouseout');
            break;
    }
}
btn.onclick = handler;
btn.onmouseover = handler;
btn.onmouseout = handler;

事件对象的eventPhase属性表示事件当前正位于事件流的哪个阶段,需要注意的是尽管“处于目标”发生在冒泡阶段,但是eventPhase仍然一支等于2,当eventPhase等于2时,this、target、currentTarget始终是相等的。

注意:只有在事件处理程序执行期间,event对象才会存在,一旦事件处理程序执行完成,event对象就会被销毁。

 

IE中的事件对象

与访问DOM中的event对象不同,要访问IE中的event对象有几种不同的方式,取决于指定事件处理程序的方法。在使用DOM0级方法添加事件处理程序时,event对象作为window对象的一个属性存在。

var btn = document.getElementById('btn');
btn.onclick = function() {
    var event = window.event;
    alert(event.type);
}

IE9+中event对象也会作为参数被传入到事件处理程序中,但是IE9和IE10中参数event和window.event并不是同一个对象,而IE11中参数event和window.event为同一个对象。

var btn = document.getElementById('btn');
btn.onclick = function(event) {
    var event1 = window.event;
    alert(event === event1);  //IE11中为true
}

如果事件处理程序是使用attachEvent添加的,那么就会有一个event对象传入事件处理函数中,同时我们也可以通过window对象来访问event对象,但是它们是不相等的。

常用属性和方法

属性方法      类型  读/写  说明

cancelBubble   Boolean 读/写  默认值为false,将其设置为true可以消除事件冒泡

returnValue     Element 读/写   默认值为true,将其设置为false可以取消事件的默认行为

srcElement    Element 只读  事件的目标(相当于DOM中target属性)

type        String  只读  被触发的事件的类型

因为使用attachEvent添加的事件处理程序中this指向window,所以我们通常使用srcElement来代替this。

跨浏览器的事件对象

var EventUtil = {
    // 阻止事件 (主要是事件冒泡,因为IE不支持事件捕获)
    stopPropagation : function(ev) {
        if (ev.stopPropagation) {
            ev.stopPropagation();
        } else {
            ev.cancelBubble = true;
        }
    },
    // 取消事件的默认行为
    preventDefault : function(event) {
        if (event.preventDefault) {
            event.preventDefault();
        } else {
            event.returnValue = false;
        }
    },
    // 获取事件目标
    getTarget : function(event) {
        return event.target || event.srcElement;
    },
    // 获取event对象的引用
    getEvent : function(event) {
        return event ? event : window.event;
    }
}

 

事件代理

因为事件有冒泡机制,所有子节点的事件都会顺着父级节点跑回去,所以我们可以通过监听父级节点来实现监听子节点的功能,这就是事件代理。

使用事件代理主要有两个优势:

  • 减少事件绑定,提升性能。之前你需要绑定一堆子节点,而现在你只需要绑定一个父节点即可。减少了绑定事件监听函数的数量。
  • 动态变化的 DOM 结构,仍然可以监听。当一个 DOM 动态创建之后,不会带有任何事件监听,除非你重新执行事件监听函数,而使用事件监听无须担忧这个问题。
addEvent(ul2, 'click', handler)
function addEvent(element, type, handler) {
    if (element.addEventListener) {
        element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
        element.attachEvent('on' + type, handler);
    } else {
        element['on' + type] = handler;
    }
}
function handler(ev) {
    var ev = ev || event;
    var target = ev.target || ev.srcElement;
  //找到a元素
    if (target.nodeName.toLowerCase() == 'a') {
          //a添加的事件
     }
}

jQuery的写法:

$('#ul1 a').on('click', function(){
    alert('正在监听');
});
//改为
$('#ul2').on('click', 'a', function(){
    alert('正在监听');
});

 

总结:

1. addEventListener()和attachEvent()的区别

  • addEventListener(type,handler,capture)有三个参数,其中type是事件名称,如click,handler是事件处理函数,capture是否使用捕获,是一个布尔值,一般为false,这是默认值,所以第三个参数可以不写。attachEvent('on'+type,handler)有两个参数,其中type是事件名称,如click,第一个参数必须是onxxxx,handler是事件处理函数,IE6 IE7 IE8不支持事件捕获,只支持事件冒泡。
  • addEventListener绑定的事件是先绑定先执行,attachEvent绑定的事件是先绑定后执行
  • 使用了attachEvent或detachEvent后事件处事函数里面的this指向window对象,而不是事件对象元素

2. 解决attchEvent事件处理函数中 this指向window的方法

1) 使用事件处理函数.apply(事件对象,arguments)
这种方式的缺点是绑定的事件无法取消绑定,原因上面已经说了,匿名函数和匿名函数之间是互不相等的。

var object=document.getElementById('xc');
function handler(){
    alert(this.innerHTML);
}
object.attachEvent('onclick',function(){
    handler.call(object,arguments);
});

2) 使用事件源代替this关键字
以下代码仅适用于IE6 IE7 IE8,这种方式完全忽略this关键字,但写起来稍显麻烦。

function handler(e){
    e = e||window.event;
    var _this = e.srcElement||e.target;
    alert(_this.innerHTML);
}
var object = document.getElementById('xc');
object.attachEvent('onclick',handler);
3) 写一个函数完全代替attachEvent/detachEvent,并且支持所有主流浏览器、解决IE6 IE7 IE8事件绑定导致的先绑定后执行问题。
注意,本函数是全局函数,而不是DOM对象的成员方法。 
/*
 * 添加事件处理程序
 * @param object object 要添加事件处理程序的元素
 * @param string type 事件名称,如click
 * @param function handler 事件处理程序,可以直接以匿名函数的形式给定,或者给一个已经定义的函数名。
 * @param boolean remove 是否是移除的事件,本参数是为简化下面的removeEvent函数而写的,对添加事件处理程序不起任何作用
*/
function addEvent(object,type,handler,remove){
    if(typeof object != 'object' || typeof handler != 'function') return;
    try{
        object[remove ? 'removeEventListener' : 'addEventListener'](type,handler,false);
    } catch( e ){
        var i, l, xc = '_' + type;
        object[xc] = object[xc] || [];
        if(remove){
            l = object[xc].length;
            for(i = 0;i < l;i++){
                if(object[xc][i].toString() === handler.toString()){
                    object[xc].splice(i,1);
                }
            }
        } else{
            l = object[xc].length;
            var exists = false;
            for(i = 0;i < l;i++){                                                
                if(object[xc][i].toString() === handler.toString()) {
                    exists = true;
                }
            }
            if(!exists) object[xc].push(handler);
        }
        object['on' + type] = function(){
            l = object[xc].length;
            for(i = 0;i < l;i++){
                object[xc][i].apply(object,arguments);
            }
        }
    }
}
/*
* 移除事件处理程序
*/
function removeEvent(object,type,handler){
    addEvent(object,type,handler,true);
}
posted @ 2016-08-24 19:45  MarcoHan  阅读(1893)  评论(2编辑  收藏  举报