首页 > 代码库 > js学习笔记:事件——事件流、事件处理程序、事件对象

js学习笔记:事件——事件流、事件处理程序、事件对象

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

事件流

事件流描述的是从页面中接受事件的顺序。

事件冒泡

IE的事件流叫做事件冒泡,即事件开始是由最具体的元素接收,然后逐级向上传播到较为不具体的节点(文档)。
如果点击了页面中的一个div元素,那么这个click事件可能会按照如下顺序传播:

  • < div>
  • < body>
  • < html>
  • document

也就是说,click事件首先在div元素上发生,然后click事件沿DOM树向上传播,在每一级节点上都会发生,直至传播至document对象。

技术分享

所有现代浏览器都支持冒泡事件。

事件捕获

Netscape团队提出的另一种事件流叫做事件捕获。事件捕获的思想是不太具体的节点应该更早接收到事件,最具体的节点应该最后接收到事件。
事件捕获的用意在于事件到达预定目标之前捕获它。

这时,单击div元素就会以下列顺序触发click事件:

  • document
  • < html>
  • < body>
  • < div>

事件捕获过程中,document对象首先接受click事件,然后事件沿DOM树依次向下,一直传播到事件的实际目标,即div元素。

技术分享

虽然大多数浏览器都支持事件捕获,但很少有人使用。

DOM事件流

DOM2级事件规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。

  • 首先发生的是事件捕获,为截获事件提供了机会。
  • 然后是实际的目标接收到事件。
  • 然后冒泡阶段发生,事件又传播回文档。

技术分享

在DOM事件流中,实际的目标在捕获阶段不会接受到事件。这意味着在捕获阶段,事件从document到< html>再到< body>后就停止了。
下一个阶段是“处于目标”阶段,于是事件在div元素上发生,并在事件处理中被看成冒泡阶段的一部分。

即使DOM2级事件明确要求捕获阶段不会涉及事件目标,但大多数浏览器都会在捕获阶段触发事件对象上的事件,结果就是有两个机会在目标对象上面操作事件。

事件处理程序

响应某个事件的函数就叫做事件处理程序(或事件侦听器)。
事件处理程序的名字以“on”开头,因此click事件的事件处理程序就是onclick,load事件的事件处理程序就是onload。
为事件指定处理程序的方式有好几种。

HTML事件处理程序

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

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

当单击这个按钮时,就会显示一个警告框。这个操作是通过指定onclick特性并将一些javascript代码作为它的值来定义的。由于这个值是javascript,因此不能在其中使用未经转义的HTML语法字符。

在HTML中定义的事件处理程序可以包含要执行的具体动作,也可以调用在页面其他地方定义的脚本。

<script type="text/javascript">
    function showMessage(){
        alert("hello world")
    }
</script>
<input type="button" value="click me" onclick="showMessage()" />

在这个例子中,单击按钮就会调用showMessage函数。这个函数是在一个独立的< script>元素中定义的,当然也可以被包含在一个外部文件中。事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码。

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

<input type="button" value="click me" onclick="alert(event.type)" />

通过event变量,可以直接访问事件对象。

在这个函数内部,this值等于事件的目标元素。

<input type="button" value="click me" onclick="alert(this.value)" />

关于这个动态创建的函数,另一个有意思的地方就是它扩展作用域的方式。这个函数使用with像下面这样扩展作用域:

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

如此一来,事件处理程序可以直接访问自己的属性:

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

如果当前元素是一个表单输入元素,则作用域中还会包含访问表单元素的入口。

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

这样的扩展方式,无非就是想让事件处理程序无需引用表单元素就能访问其他表单字段。

<form method="post">
    <input type="text" name="username" value="">
    <input type="button" value="echo username" onclick="alert(username.value)">
</form>

注意,这个例子中直接引用了username元素。

但是,在HTML中指定事件处理程序有两个缺点:

  • 存在一个时差问题。用户可能在HTML元素一出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件。
    以前面的例子来说明,假设showMessage函数是在按钮下方,页面的最底部定义的。如果用户在页面解析showMessage( )之前就单击了按钮,就会引发错误。为此,很多HTML事件处理程序都会被封装到一个try-catch块中,一遍错误不会浮出水面。

  • 这样扩展事件处理程序的作用域链在不同浏览器中会导致不同结果。

  • HTML代码与javascript代码紧密耦合。如果要更换事件处理程序,就要改动两个地方:HTML代码和javascript代码。这正是大家摒弃HTML事件处理程序,转而使用javascript指定事件处理程序的原因所在。

DOM0级事件处理程序

通过javascript指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。
每个元素(包括window和document)都有自己的事件处理程序属性,这些属性通常全部小写,例如onclick。将这种属性的值设置为一个函数,就可以指定事件处理程序:

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

在此,我们通过文档对象取得了一个按钮的引用,然后为它指定了onclick事件处理程序。
但要注意,在这些代码运行以前不会指定事件处理程序,因此如果这些代码在页面中位于按钮后面,就有可能在一段时间内怎么单击都没有反应。

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

var btn = document.getElementById("myBtn");
btn.onclick = function(){
    alert(this.id);  //"myBtn"
}

不仅仅是id,实际上可以在事件处理程序中通过this访问元素的任何属性和方法。
以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。

也可以删除通过DOM0级方法指定的事件处理程序,只要像下面这样将事件处理程序属性的值设置为null即可:

btn.onclick = null;  //删除事件处理程序

将事件处理程序设置为null后,再单击按钮将不会有任何动作发生。

如果使用HTML指定事件处理程序,那么onclick属性的值就是一个包含着在同名HTML特性中指定的代码的函数。将相应的属性设为null,也可以删除以这种方式指定的事件处理程序。

DOM2级事件处理程序

DOM2级事件定义了2个方法:

  • addEventListener()
  • removeEventListener()

所有DOM节点中都包含这两个方法,并且它们都接受3个参数:

  • 要处理的事件名
  • 作为事件处理程序的函数
  • 一个布尔值
    • true:表示在捕获阶段调用事件处理程序
    • false:表示在冒泡阶段调用事件处理程序

要在按钮上为click事件添加事件处理程序,可以使用下列代码:

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

上面的代码为一个按钮添加了onclick事件处理程序,而且该事件会在冒泡阶段被触发。
与DOM0级方法一样,这里添加的事件处理程序也是在其依附的元素的作用域中运行,因此this代表该元素。

使用DOM2级方法添加事件处理程序的主要好处是可以添加多个事件处理程序。

btn.addEventListener("click",function(){
    alert(this.id);
},false);
btn.addEventListener("click",function(){
    alert("hello world");
},false);

这里为按钮添加了两个事件处理程序,这两个事件处理程序会按照添加他们的顺序触发,因此首先会显示元素的ID,其次会显示“hello world”消息。

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

btn.addEventListener("click",function(){
    alert(this.id);
},false);

btn.removeEventListener("click",function(){ //没有用
    alert(this.id);
},false);

这个例子中,在移除事件处理程序时,看似使用了相同的参数,但实际上第二个参数与传入addEventListener()中的是完全不同的函数
传入removeEventListener()中的事件处理程序函数必须与传入addEventListener()中的完全相同

var handler = function(){
    alert(this.id);
}
btn.addEventListener("click",handler false);

btn.removeEventListener("click",handler ,false); //有效!

重写后的这个列子可以奏效,因为在addEventListener()和removeEventListener()中使用了相同的函数。

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

IE事件处理程序

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

要使用attachEvent()为按钮添加一个事件处理程序,可以使用以下代码:

btn.attachEvent("onclick",function(){
    alert("hello world");
});

注意,attachEvent()的第一个参数是“onclick”,而非DOM方法中的“click”。

在IE中使用attachEvent()与使用DOM0级方法的主要区别在于事件处理程序的作用域。在使用DOM0级方法的情况下,事件处理程序会在其所属元素的作用域内运行。在使用attachEvent()方法的情况下,事件处理程序会在全局作用域中运行,因此this等于window。

btn.attachEvent("onclick",function(){
    alert(this == window); //true
});

与addEventListener()类似,attachEvent()方法也可以用来为一个元素添加多个事件处理程序。

btn.attachEvent("onclick",function(){
    alert("clicked"); 
});
btn.attachEvent("onclick",function(){
    alert("hello world"); 
});

使用attachEvent为同一个按钮添加了两个不同的事件处理程序。但与DOM事件不同的是,这些事件处理程序不是以添加他们的顺序执行,而是以相反的顺序被触发。 单击这个按钮,首先会看到“hello world”,然后才是“clicked”。

使用attachEvent()添加的事件可以通过detachEvent()来移除,条件是必须提供相同的参数。这也意味着添加的匿名函数将不能被移除。不过,只要能够将对相同函数的引用传给detachEvent(),就可以移除相应的事件处理程序。

var handler = function(){
    alert(this.id);
}
btn.attachEvent("onclick",handler );
btn.detachEvent("onclick",handler );

这个例子将保存在变量handler中的函数作为事件处理程序。因此,后面的detachEvent()可以使用相同的函数来移除事件处理程序。

支持IE事件处理程序的浏览器有IE和opera。

跨浏览器的事件处理程序

可以自己编写跨浏览器的事件处理程序,只要在适当的地方使用能力检测即可。要保证处理事件的代码能在大多数浏览器下一致地运行,只需关心冒泡阶段。
第一个要创建的方法是addHandler(),他的职责是视情况分别使用DOM0级方法、DOM2级方法或IE方法来添加事件。这个方法属于一个名叫EventUtil的对象,我们将使用这个对象来处理浏览器间的差异。
addHandler接收3个参数:要操作的元素、事件名称和事件处理函数。

与addHandler()对应的方法是removeHandler(),它也接受相同的参数。这个方法的职责是移除之前添加的事件处理程序——无论该事件处理程序是采取什么方式添加到元素中的,如果其它方法无效,默认采用DOM0级方法。

var EventUtil = {
    addHandler:function(element,type,handler){
        if(element.addEventListener){
            element.addEventListner(type,handler,false);
        }else if(element.attachEvent){
            element.attachEvent("on"+type,handler);
        }else{
            element["on"+type] = handler;
        }
    },
    removeHandler:function(element,type,handler){
        if(element.removeEventListener){
            element.removeEventListner(type,handler,false);
        }else if(element.detachEvent){
            element.detachEvent("on"+type,handler);
        }else{
            element["on"+type] = null;
        }
    }
}

这两个方法首先都会检测传入的元素中是否存在DOM2级方法。如果存在DOM2级方法,则使用该方法;如果存在的是IE的方法,则采取第二种方案。最后一种可能就是DOM0级方法,此时我们使用的是方括号语法来将属性名指定为事件处理程序,或者将属性设置为null。

可以像下面这样使用EventUtil对象:

var btn = document.getElementById("myBtn");
var handler = function(){
    alert(this.id);
}
EventUtil.addHandler(btn,"click",handler);
EventUtil.removeHandler(btn,"click",handler);

addHandler()和removeHandler()没有考虑到所有的浏览器问题,例如IE中的作用域问题。不过,使用它们添加和移除事件处理程序还是足够了。
此外还要注意,DOM0级对每个事件只支持一个事件处理程序。

事件对象

在触发DOM上的某个事件时,会产生一个事件对象event,这个对象中包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。
所有浏览器都支持event对象,但支持方式不同。

DOM中的事件对象

兼容DOM的浏览器会将一个event对象传入到事件处理程序中。无论指定事件处理程序时使用什么方法(DOM0级或DOM2级),都会传入event对象。

btn.onclick = function(event){
    alert(event.type);  //"click"
}
btn.addEventListener("click",function(event){
    alert(event.type);  //"click"
},false);

这个例子中的两个事件处理程序都会弹出一个警告框,显示由event.type属性表示的事件类型。这个属性始终都会包含被触发的事件类型。(与传入addEventListener()和removeEventListener()的事件类型一致)。

在通过HTML特性指定事件处理程序时,变量event中保存着event对象。

<input type="button" value="click me" onclick="alert(event.type)" />

以这种方式提供event对象,可以让HTML特性事件处理程序与javascript函数执行相同的操作。

event对象包含与创建它的特定事件有关的属性和方法。触发的事件类型不一样,可用的属性和方法也不一样。

currentTarget、target

target在事件流的目标阶段;currentTarget在事件流的捕获,目标及冒泡阶段。只有当事件流处在目标阶段的时候,两个的指向才是一样的, 而当处于捕获和冒泡阶段的时候,target指向被单击的对象而currentTarget指向当前事件活动的对象(一般为父级)。

换句话说,event.target指向引起触发事件的元素,而event.currentTarget则是事件绑定的元素

在事件处理程序内部,对象this始终等于currentTarget的值,而target只包含事件的实际目标。如果直接将事件处理程序指定给了目标元素,则this、currentTarget、target包含相同的值。

btn.onclick = function(event){
    alert(event.currentTarget === this);  //true
    alert(event.target === this);  //true
}

由于click事件的目标是按钮,因此这三个值是相等的。
如果事件处理程序存在于按钮的父节点中,那么这些值是不相同的:

document.body.onclick = function(event){
    alert(event.currentTarget === document.body);  //true
    alert(this === document.body);  //true
    alert(event.target === document.getElementById("myBtn"));  //true
}

当单击这个例子中的按钮时,this和currentTarget都等于document.body,因为事件处理程序是注册到这个元素上的。然而target元素却等于按钮元素,因为它是click的真正目标。由于按钮上没有注册事件处理程序,结果click事件就冒泡到了document.body上了,在那里事件才得到了处理。

type

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

var handler = function(event){
    switch(event.type){
        case "click":
            alert("clicked");
            break;
        case "mouseover":
            event.target.style,backgroundColor = "red";
            break;
        case "mouseout":
            event.target.style.backgroundColor = "";
            break;
    }
};

btn.onclick = handler;
btn.onmouseover = handler;
btn.onmouseout = handler;

这个例子定义了一个名为handler的函数,用于处理3种事件:click、mouseover、mouseout。函数中通过检测event.type属性,让函数能够确定发生了什么事件,并执行相应的操作。

preventDefault()

要阻止特定事件的默认行为,可以使用这个方法。例如,链接的默认行为是被单击时会导航到其href特性指定的URL。如果想阻止这一默认行为,那么通过连接的onclick事件处理程序可以取消它。

link.onclick = function(event){
    event.preventDefault();
}

只有cancelable属性设为true的事件,才可以使用preventDefault()来取消其默认行为。

stopPropagation()

用于立即停止事件在DOM层次中的传播,即取消进一步的事件捕获或冒泡。
例如,直接添加到一个按钮的事件处理程序可以调用stopPropagation(),从而避免触发注册在document.body上面的事件处理程序。

btn.onclick = function(event){
    alert("clicked");
    event.stopPropagation();
}
document.body.onclick = function(event){
    alert("body clicked");
}

对于这个例子而言,如果不调用stopPropagation(),就会在单击按钮时出现两个警告框。使用了之后,由于click事件根本不会被传播到document.body,因此就不会触发注册在document.body上的onclick事件程序。

eventPhase

这个属性可以用来确定事件当前正位于事件流的哪个阶段。

  • 捕获阶段:eventPhase = 1;
  • 处于目标对象上:eventPhase = 2;
  • 冒泡阶段:eventPhase = 3;

要注意的是,尽管“处于目标”发生在冒泡阶段,但是eventPhase仍等于2.

btn.onclick = function(event){
    alert(event.eventPhase);  //2
}
document.body.addEventListener("click",function(event){
    alert(event.eventPhase);  //1
},true);
document.body.onclick = function(event){
    alert(event.eventPhase);  //3
}

当单击这个例子中的按钮时:

  • 首先执行的事件处理程序是在捕获阶段触发的添加到document.body中的那一个,会弹出一个警告框显示1
  • 其次会触发在按钮上注册的事件处理程序,此时eventPhase为2
  • 最后在冒泡阶段触发添加到document.body中的那一个,显示eventPhase为3。

当eventPhase等于2时,this、target、currentTarget始终都是相等的。

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

IE中的事件对象

与访问DOM中的event对象不同,要访问IE中的event对象有几种不同的方式,取决于指定事件处理程序的方法。

  • 在使用DOM0级方法添加事件处理程序时,event对象作为window随想的一个属性存在:
btn.onclick = function(){
    var event = window.event;
    alert(event.type); //"click"
}
  • 如果事件处理程序是通过attachEvent()添加的,那么就会有一个event对象作为参数被传入事件处理程序函数中:
btn.attachEvent("onclick",function(event){
    alert(event.type);   //"click"
})

在这种情况下,也可以通过window对象来访问event对象,就像使用DOM0级方法一样。不过为了方便起见,同一个对象也会作为参数传递。

  • 如果是通过HTML特性指定的事件处理程序,那么还可以通过一个名为event的变量来访问event对象(与DOM中的事件模型相同)。
<input type="button" value="click me" onclick="alert(event.type)"/>

IE的event对象同样也包含与创建它的事件相关的属性和方法。其中很多属性和方法都有对应的或者相关的DOM属性和方法。

srcElement

事件的目标,与DOM中的target属性相同。
因为事件处理程序的作用域是根据指定它的方式来确定的,所以不能认为this会始终等于事件目标,因此最好还是使用event.srcElement比较保险。

btn.onclick = function(){
    alert(window.event.srcElement == this);  //true
}

btn.attachEvent("onclick",function(event){
    alert(event.srcElement == this);  //false
})

第一个使用DOM0级方法指定的事件处理程序,srcElement属性等于this。
但在第二个事件处理程序中,由于this指向window,因此这两者的值不相同。

returnValue

默认值为true,将其设置为false就可以取消事件的默认行为,与DOM中的preventDefault()作用相同。

link.onclick = function(){
    window.event.returnValue = http://www.mamicode.com/false;
}

cancelBubble

与DOM中的stopPropagation()相同,都是用来停止事件冒泡的。由于IE不支持事件捕获,因此只能取消事件冒泡。但stopPropagation()可以同时取消捕获和冒泡。

btn.onclick = function(){
    alert("clicked");
    window.event.cancelBubble = true;
}

document.body.onclick = function(){
    alert("body clicked");
}

通过在onclick事件处理程序中将cancelBubble设置为true,就可以阻止时间通过冒泡而触发document.body中注册的事件处理程序。结果就是,单击按钮之后,只显示一个警告框。

跨浏览器的事件对象

虽然DOM和IE中的event对象不同,但基于它们之间的相似性依旧可以拿出跨浏览器的方案来。
可以对前面的EventUtil对象加以增强:

var EventUtil = {
    getEvent:function(event){
        return event?event:window.event;
    },
    getTarget:function(event){
        return event.target||event.srcElement;
    },
    preventDefault:function(event){
        if(event.preventDefault){
            event.preventDefault();
        }else{
            event.returnValue = http://www.mamicode.com/false;
        }
    },
    stopPropagation:function(event){
        if(event.stopPropagation){
            event.stopPropagation();
        }else{
            event.cancelBubble = true;
        }
    }
}

可以像下面这样使用:

btn.onclick = function(event){
    event = EventUtil.getEvent();
    var target = EventUtil.getTarget(event);
    EventUtil.preventDefault(event);
    EventUtil.stopPropagation(event);
}

别忘了由于IE不支持事件捕获,因此stopPropagation在跨浏览器的情况下,只能用来阻止事件冒泡。

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    js学习笔记:事件——事件流、事件处理程序、事件对象