13 事件

本章内容

  • 理解事件流
  • 使用事件处理程序
  • 不同的事件类型

JavaScript 与 HTML 之间的交互是通过事件实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。

13.1 事件流

事件流描述的是从页面中接收事件的顺序。IE 的事件流是事件冒泡流,而 Netscape Communicator 的事件流失事件捕获流。

13.1.1 事件冒泡

IE 的事件流叫做事件冒泡,即事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点。以下面的 HTML 页面为例:

<!DOCTYPE html>
<html>
<head>
  <title>event bubbling example</title>
</head>
<body>
  <div>click me</div>
</body>
</html>

如果单击了页面中的<div>元素,那么这个click事件就会按照如下顺序传播:

  1. <div>
  2. <body>
  3. <html>
  4. document

所有现代浏览器都支持事件冒泡,但在具体实现上还是有一些差别。

13.1.2 事件捕获

事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前捕获它。前述例子会按下列顺序触发click事件。

  1. document
  2. <html>
  3. <body>
  4. <div>

虽然事件捕获是 Netscape Communicator 唯一支持的事件流模型,但 其他主流浏览器目前也都支持这种事件流模型。
建议放心地使用事件冒泡,在有特殊需要时再使用事件捕获。

13.1.3 DOM 事件流

“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发送的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。
在 DOM 事件流中,实际的目标(<div>元素)在捕获阶段不会接收到事件。也就是说,事件从document<html>再到<body>后就停止了。下一个阶段是“处于目标”阶段,于是事件在<div>上发生,并在事件处理中被看成冒泡阶段的一部分。然后,冒泡阶段发生,事件又传播回文档。
多数支持 DOM 事件流的浏览器都实现了一种特定的行为:即使“DOM2 级事件”规范明确要求捕获阶段不会涉及事件目标,但高版本浏览器都会在捕获阶段触发事件对象上的事件。结果,就是有两个机会再目标对象上面操作事件。

13.2 事件处理程序

事件就是用户或浏览器自身执行的某种动作。诸如clickloadmouseover,都是事件的名字。而响应某个事件的函数就叫做事件处理程序(或事件侦听器)。事件处理程序的名字以“on”开头,因此click事件的事件处理程序就是onclickload事件的事件处理程序就是onload。为事件指定处理程序的方式有好几种。

13.2.1 HTML 事件处理程序

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

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

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

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

事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码。
这样指定事件处理程序具有一些独到之处。首先,这样会创建一个封装着元素属性值的函数。这个函数中有一个局部变量event,也就是事件对象。
不过,在 HTML 中指定事件处理程序有两个缺点。首先,存在一个时差问题。因为用户可能会在 HTML 元素一出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件。
另一个缺点是,这样扩展事件处理程序的作用域在不同浏览器中会导致不同结果。
最后一个缺点是 HTML 与 JavaScript 代码紧密耦合。故推荐使用 JavaScript 指定事件处理程序。

13.2.2 DOM0 级事件处理程序

通过 JavaScript 指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。这种为事件处理程序赋值的方法是在第四代 Web 浏览器中出现的,而且至今仍然为所有现代浏览器所支持。原因一是简单,二是具有跨浏览器的优势。要使用 JavaScript 指定事件处理程序,首先必须取得一个要操作的对象的引用。
每个元素都有自己的事件处理程序属性,这些属性通常全部小写,例如onclick。将这种属性的值设置为一个函数,就可以指定事件处理程序,如下所示:

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

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

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

以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。
可以删除通过 DOM0 级方法指定的事件处理程序,只要像下面这样将事件处理程序属性的值设置为null即可。

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

13.2.3 DOM2 级事件处理程序

“DOM2级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener()removeEventListener()。所有 DOM 节点中都包含这两个方法,并且它们都接受 3 个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。
要在按钮上为click事件添加事件处理程序,可以使用下列代码:

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

与 DOM0 级方法一样,这里添加的事件处理程序也是在其依附的元素的作用域中运行。使用 DOM2 级方法添加事件处理程序的主要好处是可以添加多个事件处理程序。如下例:

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

这里为按钮添加了俩事件处理程序。这两个事件处理程序会按照添加它们的顺序触发,因此首先会显示元素的 ID,其次会显示“Hello world!”消息。
通过addEventListener()添加的事件处理程序只能使用removeEventListener()来移除;移除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过addEventListener()添加的匿名函数将无法移除,如下例:

var btn = document.getElementById("myBtn");
btn.addEventListener("click", function () {
  alert(this.id);
}, false);
//这里省略了其他代码
btn.removeEventListener("click", function() {
  alert(this.id);  //没有用
}, false)

再看看下例:

var btn = document.getElementById("myBtn");
var handler = function () {
  alert(this.id);
};
btn.addEventListener("click", handler, false);
//这里省略了其他代码
btn.removeEventListener("click", handler, false);  //有效!

大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器。

13.2.4 IE 事件处理程序

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

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

注意,attachEvent()的第一个参数是"onclick"
在 IE 中使用attachEvent()与使用 DOM0 级方法的主要区别在于事件处理程序的作用域。在使用 DOM0 级方法的情况下,事件处理程序会在其所属元素的作用域内运行;在使用attachEvent()方法的情况下,事件,事件处理程序会在全局作用域中运行,因此this等于window。看下例:

var btn = document.getElementById('myBtn');
btn.attachEvent("onclick", function () {
  alert(this === window);  //true
});
var btn = document.getElementById('myBtn');
btn.attachEvent("onclick", function () {
  alert("clicked");
});
btn.attachEvent("onclick", function () {
  alert("Hello world!");
});

与 DOM 方法不同的是,这些事件处理程序不是以添加它们的顺序执行,而是以相反的顺序被触发。
使用attachEvent()添加的事件可以通过detachEvent()来移除。

var btn  = document.getElementById("myBtn");
var handler = function () {
  alert("clicked");
};
btn.attachEvent("onclick", handler);
//这里省略了其他代码
btn.detachEvent("onclick", handler);

13.2.5 跨浏览器的事件处理程序

为了以跨浏览器的方式处理事件,不少开发人员会使用能够隔离浏览器差异的 JavaScript 库,还有一些会自己开发最合适的事件处理的方法。自己编写代码其实也不难,只要恰当地使用能力检测即可。要保证处理事件的代码能在大多数浏览器下一致地运行,只需关注冒泡阶段。
第一个要创建的方法是addHandler(),它的职责是视情况分别使用 DOM0 级方法、DOM2 级方法或 IE 方法来添加事件。这个方法属于一个名叫EventUtil的对象,本书将使用这个对象来处理浏览器间的差异。addHandler()方法接受 3 个参数:要操作的元素、事件名称和事件处理程序。
addHandler()对应的方法是removeHandler(),它也接受相同的参数。这个方法的职责是移除之前添加的事件处理程序,默认采用 DOM0 级方法。

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.removeEventListener(type, handler, false);
  } else if (element.detachEvent) {
    element.detachEvent("on" + type, handler);
  } else {
    element["on"+type] = null;
  }
} 

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

var btn = document.getElementById("myBtn");
var handler = function () {
  alert("clicked");
};
EventUtil.addHandler(btn, "click", handler);
//这里省略了其他代码
EventUtil.removeHandler(btn, "click", handler);

addHandler()removeHandler()没有考虑到所有的浏览器问题,例如在 IE 中的作用域问题。

13.3 事件对象

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

13.3.1 DOM 中的事件对象

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

var btn = document.getElementById("myBtn");
btn.onclick = function (event) {
  alert(event.type);  //"click"
};
btn.addEventListener('click', function(event) {
  alert(event.type);  //"click"
}, false);

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

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

如果事件处理程序存在于按钮的父节点中,那么这些值是不相同的。

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

在需要通过一个函数处理多个事件时,可以使用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.onclick = handler;
btn.onmouseover = handler;
btn.onmouseout = handler;

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

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

只有cancelable属性设置为true的事件,才可以使用preventDefault()来取消其默认行为。
另外,stopPropagation()方法用于立即停止事件在 DOM 层次中的传播,即取消进一步的事件捕捉或冒泡。例如:

var btn = document.getElementById('myBtn');
btn.onclick = function(event) {
  alert('Clicked');
  event.stopPropagation();
};
document.body.onclick = function(event) {
  alert('body clicked');
};

事件对象的eventPhase属性,可以用来确定事件当前正位于事件流的那个阶段。如果是在捕获阶段调用的事件处理程序,那么等于1;如果事件处理程序处于目标对象上,则等于2;如果是在冒泡阶段调用的事件处理程序,则等于3。尽管“处于目标”发生在冒泡阶段,但eventPhase仍然一直等于2

var btn = document.getElementById('myBtn');
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
};

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

13.3.2 IE 中的事件对象

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

var btn = document.getElementById("myBtn");
btn.onclick = function () {
  var event = window.event;
  alert(event.type);  //"click"
};

13.3.3 跨浏览器的事件对象

IE 中event对象的全部信息和方法 DOM 对象中都有,只不过实现方式不一样。

var EventUtil = {
  addHandler: function(element, type, handler) {
    //省略的代码
  },
  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 = false;
    }
  },
  removeHandler: function(element, type, handler) {
    //省略的代码
  },
  stopPropagation: function(event) {
    if (event.stopPropagation) {
      event.stopPropagation();
    } else {
      event.cancelBubble = true;
    }
  }
};
btn.onclick = function(event) {
  event = EventUtil.getEvent(event);
};

13.4 事件类型

“DOM3级事件”规定了一下几类事件。

  • UI 事件,当用户与页面上的元素交互时触发;
  • 焦点事件,当元素获得或失去焦点时触发;
  • 鼠标事件,当用户通过鼠标在页面上执行操作时触发;
  • 滚轮事件,当使用鼠标滚轮(或类似设备)时触发;
  • 文本事件,当在文档中输入文本时触发;
  • 键盘事件,当用户通过键盘在页面上执行操作时触发;
  • 合成事件,当为 IME(Input Method Editor)输入字符时触发;
  • 变动事件,当底层 DOM 结构发生变化时触发。
  • 变动名称事件,当元素或属性名变动时触发。此类事件已经被废弃,没有任何浏览器实现它们,因此本章不做介绍。

13.4.1 UI 事件

var isSupported = document.implementation.hasFeature("HTMLEvents", "2.0");
var isSupported = document.implementation.hasFeature("UIEvent", "3.0");
  1. load 事件
    当页面完全加载后(包括所有图像、JavaScript 文件、CSS 文件等外部资源),就会触发window上面的load事件。
    图像上面也可以触发load事件,无论是在 DOM 中的图像元素还是 HTML 中的图像元素。
  2. unload 事件
    这个事件在文档被完全卸载后触发。只要用户从一个页面切换到另一个页面,就会发生unload事件。而利用这个事件最多的情况是清除引用,以避免内存泄漏。
  3. resize 事件
    当浏览器窗口被调整到一个新的高度或宽度时,就会触发resize事件。这个事件在window上面触发,因此可以通过 JavaScript 或者<body>元素中的onresize特性来指定事件处理程序。
  4. scroll 事件
    虽然scroll事件是在window对象上发生的,但它实际表示的则是页面中相应元素的变化。

13.4.2 焦点事件

焦点事件会在页面获得或失去焦点时触发。利用这些事件并与document.hasFocus()方法及document.activeElement属性配合,可以知晓用户在页面上的行踪。

13.4.3 鼠标与滚轮事件

13.4.4 键盘与文本事件

1.键码
2.字符编码
3.DOM3 级变化
4.textInput事件
5.设备中的键盘事件

13.4.5 复合事件

13.4.6 变动事件

13.4.7 HTML5 事件

  1. contextmenu 事件
  2. beforeunload 事件
  3. DOMContentLoaded 事件
  4. readystatechange 事件
  5. pageshow 和 pagehide 事件
  6. hashchange 事件

13.4.8 设备事件

13.4.9 触摸与手势事件

13.5 内存和性能

13.5.1 事件委托

对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡。

13.5.2 移除事件处理程序

btn.onclick = function() {
  btn.onclick = null;
  document.getElementById("myDiv").innerHTML = "Processing...";
}

13.6 模拟事件

13.7 小结

在使用事件时,需要考虑如下一些内存与性能方面的问题。

  • 有必要限制一个页面中事件处理程序的数量,数量太多会导致占用大量内存,而且也会让用户感觉页面反应不够灵敏。
  • 建立在事件冒泡机制之上的事件委托技术,可以有效地减少事件处理程序的数量。
  • 建议在浏览器卸载页面之前移除页面中的所有事件处理程序。
    可以使用 JavaScript 在浏览器中模拟事件。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 事件流: 事件流:页面接收事件的顺序。 IE定义的:事件冒泡流(由最具体的元素依次传播到DOM树的最上层的Docu...
    xiaoguo16阅读 576评论 0 0
  • 13.1 事件流 “DOM2级事件”规定事件流包括3个阶段:事件捕获阶段,处于目标阶段,事件冒泡阶段。事件捕获表示...
    Elevens_regret阅读 406评论 0 0
  • JavaScript 程序采用了异步事件驱动编程模型。在这种程序设计风格下,当文档、浏览器、元素或与之相关的对象发...
    劼哥stone阅读 1,250评论 3 11
  • 书接上回,儒家核心价值标准是什么?忠孝仁义,礼仪廉耻。 宋江束发受教,耳濡目染,浸润甚深。这和他所倡导的替天行道宗...
    衡州布衣阅读 157评论 0 0
  • 【1】:今天会很残酷,明天会很残酷,后天会很美好,但大部分人会死在明天晚上。 【2】:永远不要跟别人比幸运,我从来...
    lightair_2017阅读 162评论 0 0