1

事件

纲要

  1. 理解事件流

  2. 使用事件处理程序

  3. 不同的事件类型

javascript和html的交互是通过事件实现的。事件就是文档或浏览器窗口发生的一些特定交互瞬间。可以使用侦听器(事件处理程序)预定事件,以便事件发生时执行相应的代码。

1 事件流

浏览器发展到第四代时(IE4和Netscape communicator4)都认为,当你单击某个按钮时,在单击这个按钮的同时,你也单击了包含这个按钮的容器甚至整个页面。就如同你用手指指向一同心圆的圆心,你指向的不单是一个圆,而是以这个圆心为圆心的所有圆。
所谓事件流,指的是页面接收事件时的顺序。IE提出的事件流是事件冒泡流,Netscape communicator提出的事件流是事件捕获流。

1.1 事件冒泡流

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

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
<body>
  <div id='myDiv'>click me</div>
</body>
</html>

上段代码,按照冒泡流的说法,就是你点击div时,沿着DOM树,你也点击了body,html,document。

1.2 事件捕获流

接收事件的顺序,由不太具体的节点先接收,逐级向下传到具体的节点。其用意就是在事件到达目标之前先捕获他。

1.3 DOM事件流

'DOM2级事件',规定事件流包括三个阶段。事件捕获阶段、处于目标阶段、事件冒泡阶段。在事件冒泡阶段对事件做出响应。单击<div>元素会按下图所示顺序触发事件

图片描述

在DOM事件流中,实际的目标(<div>元素)在捕获阶段不会接收到事件,这意味着在捕获阶段,事件从document到<html>再到<body>就停止了。下一个阶段是’处于目标‘阶段,于是事件在<div>上发生,并在事件处理中被看成是冒泡阶段的一部分,然后冒泡阶段发生,事件又传播回文档。

多数支持DOM事件流的浏览器都实现了一种特定的行为:即使"DOM2级事件"规范明确要求捕获阶段不会涉及事件目标,但IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都会在捕获阶段触发事件对象上的事件。结果,就有两个机会在目标对象上面操作事件。(注意,IE8及更早版本不支持DOM事件流)

  1. 下面这段程序,可以看出在捕获阶段(机会1)和冒泡阶段(机会2)都在目标对象上操作(触发)了事件

  2. 运行这段程序,分别点击红黄绿区域,感受下DOM事件执行的顺序:(在DOM2级事件处理程序中,绑定在目标元素上的事件执行顺序是按事件的注册先后顺序执行的,所以alert3.2在3.1之前。)

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
<body>
    <div id="box" style="background:red;width:200px;height:200px;">
        <div id="outer" style="background:yellow;width:150px;height:150px;">
            <div id="inner" style="background:green;width:70px;height:70px;">click me</div>
        </div>
    </div>
  <script type="text/javascript">
window.onload=function(){
    var Box=document.getElementById('box');
    var outer = document.getElementById("outer");
    var inner = document.getElementById('inner');
    //true为事件捕获,false为事件冒泡
    Box.addEventListener("click",function(){
        alert('1');
    },true)

    Box.addEventListener("click",function(){
        alert('4');
    },false)

    outer.addEventListener("click", function(){
      alert("2");
    }, true);

    inner.addEventListener("click",function(){
        alert('3.2')
    },false);

    inner.addEventListener("click", function(){
      alert("3.1");
     }, true);
    
}
  </script>
</body>
</html>


2 事件处理程序

事件就是用户或浏览器自身执行的某种动作。如click,load,mouseover等都是事件的名字。而响应某个事件的函数叫事件处理程序。事件处理程序的名字以"on"开头,因此click事件的事件处理程序就是onclick。为事件指定处理程序的方式有好几种。

2.1 HTML事件处理程序

某个元素支持的每种事件,都可以使用一个与之相应 事件处理程序 同名的 HTML 特性(属性)来指定。这个特性的值应该是能执行的Javascript代码。例如,要在按钮被单击时执行一些Javascript:<input type="button" value="Click me" onclick="alert('hello world')"/>。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
<body>
  <script type="text/javascript">
    function showMessage(){
      alert('hello world');
    }
  </script>
    <div id="box">
        <input type="button" value="Click me" onclick="showMessage()"/>
    </div>
</body>
</html>

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

  • 缺点一 时间差,HTML加载完成,但js还未加载完成(一般情况,HTML是按注册顺序加载的),如果在showMessage()函数解析前就单击事件,便会引发错误。

解决办法:将HTML事件处理程序封装在一个try-catch块中

<input type="button" value="Click me" onclick="try{showMessage();}catch(ex){}"/>

/番外篇/

try...catch 的作用是测试代码中的错误。

语法:

try
{
   //在此运行代码
   tryStatements
}
catch(err)
{
   //在此处理错误
  catchStatements 
}

参数:

tryStatements 必选项,可能发生错误的代码
catchStatements 可选项,发生tryStatements中关联的错误后执行的代码

示例一,请点击确定/取消

<html>
<head>
<script type="text/javascript">
var txt=""
function message()
{
try
  {
    //正确是 alert,错误代码即 adddlert
  adddlert("Welcome guest!")
  }
catch(err)
  {
  txt="There was an error on this page.\n\n"
  txt+="Click OK to continue viewing this page,\n"
  txt+="or Cancel to return to the home page.\n\n"
  //confirm(message)---要在 window 上弹出的对话框中显示的纯文本(而非 HTML 文本)
  if(!confirm(txt))
    {
    document.location.href="https://segmentfault.com/u/xiakejie"
    }
  }
}
</script>
</head>
<body>
<input type="button" value="View message" onclick="try{message();}catch(err){}" />
</body>
</html>

语法:

try  {  
   tryStatements}  
catch(exception){  
   catchStatements}  
finally  {  
   finallyStatements}

参数:
finallyStatements:可选项,当tryStatements,catchStatements皆抛出错误执行的代码

示例二 与稍后讲的跨浏览器处理程序比较,与if...elaeif...else比较(对这里的方法若不理解,后面会讲到)

<html>
<head>
</head>
<body>
<input id='b1' type='button' value='按钮'/>
<script>
(function(){
  var oBtn=document.getElementById("b1");
  function mto(){
  alert("123");
  };

  try 
  {
  oBtn.attachEvent("onclick",mto);
  }
  catch(e)
  {
  oBtn.addEventListener("click",mto,false);
  }
  finally //DOM0级事件处理程序  
  {  
  oBtn.onclick=mto;
   }

 })();
</script>
</body>
</html>

  • 缺点二 HTML与javascript代码紧密耦合,如果要更换事件处理程序,HTML部分和javascript部分都要修改。(另外一个缺点会在后续另篇文章解析)

2.2 DOM0级事件处理程序

通过Javascript指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。这种为事件处理程序赋值的方法,在第四代WEB浏览器出现,至今所有的浏览器都支持。要使用javascript指定事件处理程序,首先必须取得一个要操作的对象的引用

每个元素(包括window document)都有自己的事件处理程序属性,这些属性通常全部小写,例如onclick。如下

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
<body>
  <div id="box">
      <input type="button" value="Click me" id="btn" />
  </div>
  <script type="text/javascript">
    function showMessage(){
      alert('DOM0级事件处理程序');
    }
    var oBtn=document.getElementById('btn');
    oBtn.onclick=showMessage;
  </script>
</body>
</html>

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

2.2.1 番外篇

很多同学或许纳闷 oBtn.onclick=showMessage;oBtn.onclick=showMessage(); 区别

showMessage是一个函数名,ECMAscript中函数是一个对象,函数名则是一个指向函数对象的指针,使用不带括号的函数名是访问函数指针,而不是调用函数。若要调用则带括号。

我是这样理解的 var funName=function(){...}; 是一个函数表达式,这个函数表达式是将一个匿名函数赋值给一个变量。在JS是面向对象编程语言,变量也是对象。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
<body>
  <script type="text/javascript">
    var fnName=function(){
    alert('Hello World');
    }();
  </script>
</body>
</html>

上例我们看出函数表达式后加()直接调用。这样理解 var funName=function(){...}; 便是一个函数表达式,oBtn.onclick=showMessage(); 这样便是把他看成一个函数表达式,并且直接执行这个函数,而不是DOM0级事件处理程序。这块知识我在另一篇文章中有详细讲解 https://segmentfault.com/a/11...

2.2.2 DOM0级事件处理程序的作用域

使用DOM0级方法指定事件处理程序被认为是元素的方法(之所以称元素的方法,是因为使用DOM0级事件处理程序必须首先获得一个操作对象,这个操作对象也就是DOM元素)。因此这时事件处理程序是在元素的作用域中进行。那么,程序中的this引用当前元素。在事件处理程序中可以通过this访问元素的任何属性和方法。DOM0级事件处理程序会在事件流的冒泡阶段被处理。

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

2.2.3 删除DOM0级事件处理程序

oBtn.onclick=null;

2.3 DOM2级事件处理程序

DOM2级事件处理程序定义了两个方法:用于处理指定和删除事件处理程序的操作,addEventListener()和removeEventListener()。所有的DOM节点都包含这两个方法。都接收三个参数:要处理的事件名,作为事件处理程序的函数和布尔值。true表示在捕获阶段调用事件处理程序,false表示在冒泡阶段调用事件处理程序。使用DOM2级方法指定事件处理程序也被认为是元素的方法,事件处理程序也是在元素的作用域中运行的,也可以通过this访问元素的任何属性和方法。

2.3.1 DOM2级事件处理程序优点

可以添加多个事件处理程序。并且按添加的顺序触发。

var oBtn=docunment.getElementById('myBtn');
oBtn.addEventListener('click',function(){alert(this.id)},false);
oBtn.addEventListener('click',function(){alert('hello')},false);

2.3.1 DOM2级事件处理程序的移除

通过addEventListener()添加的事件处理程序只能用removeEventListener()移除,并且传入参数要与添加处理程序参数完全相同。意味着通过abbEventListener()添加的匿名函数无法被移除

为了兼容各种浏览器我们大多数将事件处理程序添加到事件流的冒泡阶段。除非为了在事件到达目标前捕获他,才将事件处理程序添加到捕获阶段。

2.3.2 支持的浏览器

IE9,Firefox,Safari,Chrome和Opers支持DOM2级事件处理程序。

2.4 IE事件处理程序

IE事件处理程序,指定和删除事件处理程序方法:attachEvent()和detachEvent().接受相同的两个参数,事件处理程序名称与事件处理程序函数,(注意DOM2级第一个参数是事件名)。通过attachEvent()添加的事件处理程序都会被加到冒泡阶段,是因为IE8及更早版本只支持冒泡事件流

使用attachEvent()为按钮添加一个事件处理程序.

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

IE事件处理程序与DOM0级事件处理程序主要区别:事件处理程序的作用域。使用DOM0级方法,事件处理程序是在其所属元素内运行;attachElent()方法时,事件处理程序是在全局作用域中运行,this值因此等于Window. 在编写跨浏览器代码时牢记这一区别(跨浏览器稍后会说到)

同DOM2级事件处理程序一样,移除事件只能通过detachEvent()移除,并且传入的参数要一样。

2.4.1支持的浏览器

IE和Opera

2.5跨浏览器事件处理程序

回顾一下前面的几种事件处理程序。HTML事件处理程序,DOM0级事件处理程序,DOM2级事件处理程序,IE事件处理程序。

HTML事件处理程序,适合所有浏览器;DOM0级,适合所有浏览器;DOM2级,适合IE9,Firefox,Safari,Chrome和Opers;IE事件处理程序,适合IE和Opera。

2.5.1 跨浏览器事件处理程序

为了以跨浏览器方式处理事件,方法一,使用能够隔离浏览器差异的javascript库。方法二,自己开发最适合的处理方法。我们这里使用的便是方法二。运用能力检测思想。我们要保证处理事件的代码在大多数浏览器下一致的运行,只需关注冒泡阶段(因为所有现代浏览器都支持事件冒泡)。

在讲跨浏览器事件处理程序之前,我们先温习一下Object。创建Object实例的方式有两种

  • 使用NEW操作符后跟Object实例,var person = new Object(); person.name="liMing"; person.age = 29;

  • 第二种是使用对象字面量表示法,var person = {name:"liMing",age:29};

访问对象属性方法也有两种

  • 点表示法。alert(person.name);

  • 第二种是是方括号表示法,alert(person['name']);//优点,可以通过变量来访问属性

创建一个对象eventUtil,对象包含两个方法,一个方法给元素添加事件addHeadler(),另一个方法给元素去除事件removeHeader()。方法接受三个参数,要操作的元素,事件名称,和事件处理程序函数。

var EventUtil = {
  addHandler : function (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;
    }
  },
  removeHandler : function(element.type,handler){
    if (element.removeEventListener) {
      element.addEventListener(type,handler,false);
    }else if (element.attachEvent) {
      element.attachEvent('on'+type,handler);
    }else {
      element['on'+type]=null;
    }
  } 
};

引用:EventUnit.addHandler(element,'click',handler);

简析:可以将EventUnil看成使用对象字面量法创建的Object实例。addHandler,removeHandler看成对象的属性。在ECMAScript中函数名本身就是变量,所以函数也可以作为值来使用,所以可以讲function(evement,type,handler){}看作是属性的值。因为ECMAScript中函数可以传进来多个参数,参数的数据类型不限制,所以这里可以传入对象eventment,字符串type,函数/对象handler.

引用形式的简析:
创建一个对象:var person = {name:'liMing'}; 也可以var person=new Object(); person.name='liMing';
那么EventUnil.addHandler(element,'click',handler)可以理解为var EventUnil.addHandler=function(){};这一函数表达式的调用。注,以上简析均是个人理解,若不正确,希望指正。

3 事件对象

在触发DOM上的某个事件时,会产生一个事件对象Event,这个对象包含着所有与事件有关的信息。所有的浏览器都支持Event对象,但支持的方式不同。

3.1 DOM中的事件对象

无论指定事件处理程序时使用什么方法(DOM0,DOM2),都会传入Event对象。event对象包含与创建它的特定事件有关的属性和方法,触发的事件类型不一样,可用的属性和方法也不一样,不过,所有事件都会有下表列出的成员

图片描述
图片描述

3.1.1 currentTarget 与 target

在事件处理程序内部,对象this始终等于currentTarget的值,而target则只包含事件的实际目标。事件处理程序存在于按钮的父节点中(例如document.body)

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

该结果是由于按钮上没有注册事件处理程序,点击按钮时,click事件冒泡到了document.body,在那里事件才得到处理。

3.1.2 type

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

var btn = document.getElementById('myBtn');
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.click = handler;
btn.mouseover = handler;
btn.mouseout = handler;

3.1.3 阻止事件的默认行为 preventDefault()

取消特定事件的默认行为。例如,链接的默认行为就是在被单击时会导航到href特性指定的URL,如果你想阻止链接导航这一默认行为,通过链接的onclick事件处理程序取消它。

var link = document.getElementById('myLink');
link.onclick = function(event){
    event.preventDefault();
};

3.1.4 取消进一步事件的捕获或冒泡 stopPropagation()

在讲currentTarget与target时,我们知道按钮没有注册事件处理程序,click事件冒泡到了document,body上。现在我们可以阻止冒泡行为了,结果避免触发注册在document.body上的事件处理程序

  var btn = document.getElementById('myBtn');
  btn.onclick = function(event){
    event.stopPropagation();
  };
  document.body.onclick = function(event){
    alert(event.currentTarget===document.body);
    alert(this===document.body);
    alert(event.target===btn);
  };
  

3.1.5 确定事件位于当前事件流那个阶段eventPhase

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

3.2 IE中的事件对象

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

  • 使用DOM0级方法添加事件处理程序。event对象作为window对象的一个属性存在。

var btn = document,getElementById('myBtn');
btn.onclick = function(){
    var event = window.event;
    alert(event.type); //"click"
};
  • 使用IE方法添加事件处理程序,event对象作为参数传入事件处理程序中。

var btn = document,getElementById('myBtn');
btn.attachEvent ("onclick",function(event){
  alert(event.type);//"click"
});
  • 通过HTML特性指定事件处理程序,通过一个名为event变量访问event对象(与DOM中的事件模型相同)

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

3.2.1 IE中event对象的属性和方法

其中很多属性和方法都有对应的或相关的DOM属性和方法,与DOM的event对象一样,这些属性和方法也会因为事件类型的不同而不同,但所有事件对象都会包含下表所列的属性和方法
图片描述

3.2.2 事件目标
因为事件处理程序作用域是根据指定他的方式确定的,所以不能认为this始终等于事件目标。由于我的浏览器版本问题,书中的现象复现不了,所以用下程序代替。

  var btn = document.getElementById('myBtn');
  function showMes(event){
    var event = window.event||event;
    var ele = event.target||event.srcElement;
    alert(ele === this);
  };
  var eventUnit = {
    addHandler: function(element,type,handler){
      if (element.attachEvent) {
        element.attachEvent(type,handler)
      }else{
        element[type]=handler;
      }
    }
  };
  eventUnit.addHandler(btn,'onclick',showMes);

3.2.3 取消事件的默认行为,returnValue

returnValue属性相当与DOM中的preventDefault()方法,只要将returnValue设置为falsae就可以阻止事件的默认行为。同样因为浏览器版本问题,书中代码修改如下:

  var link = document.getElementById('myLink');
  function stopEvent(event){
    var event = event||window.event;
    event.returnValue = false;
    event.preventDefault();
  };
  link.onclick = stopEvent;

3.2.4 阻止事件冒泡 cancelBubble

cancelBubble属性与DOM中的stopPropagatioin()方法作用相同,IE不支持事件捕获,所以只用取消事件冒泡。事件处理程序中将cancelBubble设置为true便可以阻止事件冒泡行为。

3.3 跨浏览器的事件对象

<body>
  <input type="button" name="button" id="myBtn" value="button">
  <a href="http://www.w3school.com.cn" id="myLink">W3School</a>
  <script type="text/javascript">
  var btn = document.getElementById('myBtn');
  var link = document.getElementById('myLink');
  var EventUtil = {
    getEvent : function(event){
      return event?event:window.event;
    },
    getTarget : function(event){
      return event.target||event.srcElement;
     //return event.target?event.target:event.srcElement;
    },
    preventDefault : function(event){
      if (event.preventDefault) {
        event.preventDefault();
      }else{
        event.returnValue = false;
      }
    },
    stopPropagation : function(event){
      if (event.stopPropagation) {
        event.stopPropagation();
      }else{
        event.cancelBubble = true;
      }
    },
  };
  btn.onclick = function(event){
    event = EventUtil.getEvent(event);
  };
  link.onclick = function(event){
    event = EventUtil.getEvent(event);
    EventUtil.preventDefault(event);
    EventUtil.stopPropagation(event);
  };
  btn.onclick = function (event){
    event=EventUtil.getEvent(event);
    target = EventUtil.getTarget(event);
    alert('stopPropagation');
    alert(target);
    EventUtil.stopPropagation(event);
  };
  document.body.onclick = function(event){
    alert('body clicked');
  };
  </script>
</body>

本文参考javascript高级程序设计第三版


下客介
68 声望4 粉丝