事件

事件处理程序

DOM0 事件处理程序

let btn = document.getElementById("myBtn");
btn.onclick = function() {
  console.log(this.id);  // "myBtn"
};

点击按钮,这段代码会显示元素的 ID。这个 ID 是通过 this.id 获取的。不仅仅是 id,在事件处 理程序里通过 this 可以访问元素的任何属性和方法。以这种方式添加事件处理程序是注册在事件流的 冒泡阶段的。 通过将事件处理程序属性的值设置为 null,可以移除通过 DOM0 方式添加的事件处理程序,如下 面的例子所示:

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

把事件处理程序设置为 null,再点击按钮就不会执行任何操作了。 注意如果事件处理程序是在 HTML 中指定的,则 onclick 属性的值是一个包装相应 HTML 事件处理程序属性值的函数。这些事件处理程序也可以通过在 JavaScript 中将相应 属性设置为 null 来移除。

DOM2 事件处理程序

DOM2 Events 为事件处理程序的赋值和移除定义了两个方法:addEventListener()和 remove- EventListener()。这两个方法暴露在所有 DOM 节点上,它们接收 3 个参数:事件名、事件处理函 数和一个布尔值,true 表示在捕获阶段调用事件处理程序,false(默认值)表示在冒泡阶段调用事 件处理程序。 与 DOM0 方式类似,这个事件处理程序同样在被附加到的元素的作用域中运行。使用 DOM2 方式的主要优势是可以为同一个事件添加多个事件处理程序。来看下面的例子:

let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
  console.log(this.id);
}, false);
btn.addEventListener("click", () => {
  console.log("Hello world!");
}, false);

这里给按钮添加了两个事件处理程序。多个事件处理程序以添加顺序来触发,因此前面的代码会先 打印元素 ID,然后显示消息“Hello world!”。 通过 addEventListener()添加的事件处理程序只能使用 removeEventListener()并传入与添 加时同样的参数来移除。这意味着使用 addEventListener()添加的匿名函数无法移除,如下面的例 子所示:

let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
  console.log(this.id);
 }, false);
// 其他代码
btn.removeEventListener("click", function() {
  console.log(this.id);
}, false);
// 没有效果!

这个例子通过 addEventListener()添加了一个匿名函数作为事件处理程序。然后,又以看起来 相同的参数调用了 removeEventListener()。但实际上,第二个参数与传给 addEventListener() 的完全不是一回事。传给 removeEventListener()的事件处理函数必须与传给 addEventListener() 的是同一个,如下面的例子所示:

let btn = document.getElementById("myBtn");
let handler = function() {
  console.log(this.id);
};
btn.addEventListener("click", handler, false);
// 其他代码
btn.removeEventListener("click", handler, false); // 有效果!

这个例子有效,因为调用 addEventListener()和 removeEventListener()时传入的是同一个 函数。 大多数情况下,事件处理程序会被添加到事件流的冒泡阶段,主要原因是跨浏览器兼容性好。把事 件处理程序注册到捕获阶段通常用于在事件到达其指定目标之前拦截事件。如果不需要拦截,则不要使 用事件捕获。

跨浏览器事件处理程序

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 对象:

let btn = document.getElementById("myBtn")
let handler = function() {
  console.log("Clicked");
};
EventUtil.addHandler(btn, "click", handler);
// 其他代码
EventUtil.removeHandler(btn, "click", handler);

DOM 事件对象

属性/方法类型读/写说明

bubbles

布尔值

只读

表示事件是否冒泡

cancelable

布尔值

只读

表示是否可以取消事件的默认行为

currentTarget

元素

只读

当前事件处理程序所在的元素

defaultPrevented

布尔值

只读

true 表示已经调用 preventDefault()方法(DOM3 Events 中新增)

detail

整数

只读

事件相关的其他信息

eventPhase

整数

只读

表示调用事件处理程序的阶段:1 代表捕获阶段,2 代表到达目标,3 代表冒泡阶段

preventDefault()

函数

只读

用于取消事件的默认行为。只有 cancelable 为 true 才可以调用这个方法

stopImmediatePropagation()

函数

只读

用于取消所有后续事件捕获或事件冒泡,并阻止调用任何后续事件处理程序(DOM3 Events 中新增)

stopPropagation()

函数

只读

用于取消所有后续事件捕获或事件冒泡。只有 bubbles 为 true 才可以调用这个方法

target

元素

只读

事件目标

trusted

布尔值

只读

true 表示事件是由浏览器生成的。false 表示事件是开发者通过 JavaScript 创建的(DOM3 Events 中新增)

type

字符串

只读

被触发的事件类型

view

AbstractView

只读

与事件相关的抽象视图。等于事件所发生的 window 对象。

在事件处理程序内部,this 对象始终等于 currentTarget 的值,而 target 只包含事件的实际 目标。如果事件处理程序直接添加在了意图的目标,则 this、currentTarget 和 target 的值是一样 的。下面的例子展示了这两个属性都等于 this 的情形:

let btn = document.getElementById("myBtn");
btn.onclick = function(event) {
  console.log(event.currentTarget === this);  // true
  console.log(event.target === this);         // true
}

上面的代码检测了 currentTarget 和 target 的值是否等于 this。因为 click 事件的目标是按 钮,所以这 3 个值是相等的。如果这个事件处理程序是添加到按钮的父节点(如 document.body)上, 那么它们的值就不一样了。比如下面的例子在 document.body 上添加了单击处理程序:

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

这种情况下点击按钮,this 和 currentTarget 都等于 document.body,这是因为它是注册事件 处理程序的元素。而 target 属性等于按钮本身,这是因为那才是 click 事件真正的目标。由于按钮 本身并没有注册事件处理程序,因此 click 事件冒泡到 document.body,从而触发了在它上面注册的 处理程序。 type 属性在一个处理程序处理多个事件时很有用。比如下面的处理程序中就使用了 event.type:

let btn = document.getElementById("myBtn");
let handler = function (event) {
  switch (event.type) {
    case "click":
      console.log("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。 preventDefault()方法用于阻止特定事件的默认动作。比如,链接的默认行为就是在被单击时导 航到 href 属性指定的 URL。如果想阻止这个导航行为,可以在 onclick 事件处理程序中取消,如下 面的例子所示:

let link = document.getElementById("myLink"); 
link.onclick = function(event) {
    event.preventDefault();
}

stopPropagation()方法用于立即阻止事件流在 DOM 结构中传播,取消后续的事件捕获或冒泡。 例如,直接添加到按钮的事件处理程序中调用 stopPropagation(),可以阻止 document.body 上注 册 的事件处理程序执行。比如:

let btn = document.getElementById("myBtn");
btn.onclick = function(event) {
  console.log("Clicked");
  event.stopPropagation();
};
document.body.onclick = function(event) {
  console.log("Body clicked");
};

如果这个例子中不调用 stopPropagation(),那么点击按钮就会打印两条消息。但这里由于 click 事件不会传播到 document.body,因此 onclick 事件处理程序永远不会执行。

eventPhase 属性可用于确定事件流当前所处的阶段。如果事件处理程序在捕获阶段被调用,则 eventPhase 等于 1;如果事件处理程序在目标上被调用,则 eventPhase 等于 2;如果事件处理程序 在冒泡阶段被调用,则 eventPhase 等于 3。不过要注意的是,虽然“到达目标”是在冒泡阶段发生的, 但其 eventPhase 仍然等于 2。下面的例子展示了 eventPhase 在不同阶段的值:

let btn = document.getElementById("myBtn");
  btn.onclick = function(event) {
    console.log(event.eventPhase);   // 2
};
document.body.addEventListener("click", (event) => {
  console.log(event.eventPhase);   // 1
}, true);
document.body.onclick = (event) => {
  console.log(event.eventPhase);   // 3
};

在这个例子中,点击按钮首先会触发注册在捕获阶段的 document.body 上的事件处理程序, 显示 eventPhase 为 1。接着,会触发按钮本身的事件处理程序(尽管是注册在冒泡阶段),此时显 示 eventPhase 等于 2。最后触发的是注册在冒泡阶段的 document.body 上的事件处理程序,显示 eventPhase 为 3。而当 eventPhase 等于 2 时,this、target 和 currentTarget 三者相等。 注意 event对象只在事件处理程序执行期间存在,一旦执行完毕,就会被销毁。

事件类型

Web 浏览器中可以发生很多种事件。如前所述,所发生事件的类型决定了事件对象中会保存什么信 息。DOM3 Events 定义了如下事件类型。

  • 用户界面事件(UIEvent):涉及与 BOM 交互的通用浏览器事件。

  • 焦点事件(FocusEvent):在元素获得和失去焦点时触发。

  • 鼠标事件(MouseEvent):使用鼠标在页面上执行某些操作时触发。

  • 滚轮事件(WheelEvent):使用鼠标滚轮(或类似设备)时触发。

  • 输入事件(InputEvent):向文档中输入文本时触发。

  • 键盘事件(KeyboardEvent):使用键盘在页面上执行某些操作时触发。

  • 合成事件(CompositionEvent):在使用某种 IME(Input Method Editor,输入法编辑器)输入字符时触发。

用户界面事件

户界面事件或 UI 事件不一定跟用户操作有关。这类事件在 DOM 规范出现之前就已经以某种形 式存在了,保留它们是为了向后兼容。UI 事件主要有以下几种。

  • DOMActivate:元素被用户通过鼠标或键盘操作激活时触发(比 click 或 keydown 更通用)。 这个事件在 DOM3 Events 中已经废弃。因为浏览器实现之间存在差异,所以不要使用它。

  • load:在 window 上当页面加载完成后触发,在窗套(<frameset>)上当所有窗格(<frame>)都加载完成后触发,在<img>元素上当图片加载完成后触发,在<object>元素上当相应对象加 载完成后触发。

  • unload:在 window 上当页面完全卸载后触发,在窗套上当所有窗格都卸载完成后触发,在 <object>元素上当相应对象卸载完成后触发。

  • abort:在<object>元素上当相应对象加载完成前被用户提前终止下载时触发。

  • error:在 window 上当 JavaScript 报错时触发,在<img>元素上当无法加载指定图片时触发, 在<object>元素上当无法加载相应对象时触发,在窗套上当一个或多个窗格无法完成加载时 触发。

  • select:在文本框(<input>或 textarea)上当用户选择了一个或多个字符时触发。

  • resize:在 window 或窗格上当窗口或窗格被缩放时触发。

  • scroll:当用户滚动包含滚动条的元素时在元素上触发。<body>元素包含已加载页面的滚动条。 大多数 HTML 事件与 window 对象和表单控件有关。 除了 DOMActivate,这些事件在 DOM2 Events 中都被归为 HTML Events(DOMActivate 在 DOM2 中仍旧是 UI 事件)。

load 事件

load 事件可能是 JavaScript 中最常用的事件。在 window 对象上,load 事件会在整个页面(包括 所有外部资源如图片、JavaScript 文件和 CSS 文件)加载完成后触发。可以通过两种方式指定 load 事 件处理程序。第一种是 JavaScript 方式,如下所示:

window.addEventListener("load", (event) => {
  console.log("Loaded!");
});


window.addEventListener("load", () => {
  let image = document.createElement("img");
  image.addEventListener("load", (event) => {
    console.log(event.target.src);
  });
  document.body.appendChild(image);
  image.src = "smile.gif";
});

这个例子首先为 window 指定了一个 load 事件处理程序。因为示例涉及向 DOM 中添加新元素, 所以必须确保页面已经加载完成。如果在页面加载完成之前操作 document.body,则会导致错误。然 25 后,代码创建了一个新的<img>元素,并为这个元素设置了 load 事件处理程序。最后,才把这个元素 添加到文档中并指定了其 src 属性。注意,下载图片并不一定要把<img>元素添加到文档,只要给它设置了 src 属性就会立即开始下载。

下面的 代码展示了如何给动态创建的<script>元素指定事件处理程序:

window.addEventListener("load", () => {
  let script = document.createElement("script");
  script.addEventListener("load", (event) => {
    console.log("Loaded");
  });
  script.src = "example.js";
  document.body.appendChild(script);
});

这里 event 对象的 target 属性在大多数浏览器中是<script>节点。IE8 及更早版本不支持<script> 元素触发 load 事件。

unload 事件

与 load 事件相对的是 unload 事件,unload 事件会在文档卸载完成后触发。unload 事件一般是 在从一个页面导航到另一个页面时触发,最常用于清理引用,以避免内存泄漏。与 load 事件类似, unload 事件处理程序也有两种指定方式。第一种是 JavaScript 方式,如下所示:

window.addEventListener("unload", (event) => {
      console.log("Unloaded!");
});

触摸屏设备

iOS 和 Android 等触摸屏设备的实现大相径庭,因为触摸屏通常不支持鼠标操作。在为触摸屏设备 开发时,要记住以下事项。

  • 不支持 dblclick 事件。双击浏览器窗口可以放大,但没有办法覆盖这个行为。

  • 单指点触屏幕上的可点击元素会触发 mousemove 事件。如果操作会导致内容变化,则不会再触 发其他事件。如果屏幕上没有变化,则会相继触发 mousedown、mouseup 和 click 事件。点 触不可点击的元素不会触发事件。可点击元素是指点击时有默认动作的元素(如链接)或指定 了 onclick 事件处理程序的元素。

  • mousemove 事件也会触发 mouseover 和 mouseout 事件。

  • 双指点触屏幕并滑动导致页面滚动时会触发 mousewheel 和 scroll 事件。

contextmenu 事件

<!DOCTYPE html>
<html>
<head>
  <title>ContextMenu Event Example</title>
</head>
<body>
  <div id="myDiv">Right click or Ctrl+click me to get a custom context menu. Click anywhere else to get the default
    context menu.</div>
  <ul id="myMenu" style="position:absolute;visibility:hidden;background-color: silver">
    <li><a href="http://www.somewhere.com"> somewhere</a></li>
    <li><a href="http://www.wrox.com">Wrox site</a></li>
    <li><a href="http://www.somewhere-else.com">somewhere-else</a></li>
  </ul>
  <script>
    window.addEventListener("load", (event) => {
        let div = document.getElementById("myDiv");
        div.addEventListener("contextmenu", (event) => {
          event.preventDefault();
          let menu = document.getElementById("myMenu");
          menu.style.left = event.clientX + "px";
          menu.style.top = event.clientY + "px";
          menu.style.visibility = "visible";
        });
        document.addEventListener("click", (event) => {
          document.getElementById("myMenu").style.visibility = "hidden";
        });
      });
  </script>
</body>
</html>

移动端设备事件

orientationchange 事件

果公司在移动 Safari 浏览器上创造了 orientationchange 事件,以方便开发者判断用户的设备 是处于垂直模式还是水平模式。移动 Safari 在 window 上暴露了 window.orientation 属性,它有以 下 3 种值之一:0 表示垂直模式,90 表示左转水平模式(主屏幕键在右侧),–90 表示右转水平模式(主 屏幕键在左)。虽然相关文档也提及设备倒置后的值为 180,但设备本身至今还不支持。图 17-9 展示了 window.orientation 属性的各种值。

以下是这个事件典 型的用法:

window.addEventListener("load", (event) => {
  let div = document.getElementById("myDiv");
  div.innerHTML = "Current orientation is " + window.orientation;
  window.addEventListener("orientationchange", (event) => {
    div.innerHTML = "Current orientation is " + window.orientation;
  });
})

触摸及手势事件

触摸事件

iPhone 3G 发布时,iOS 2.0 内置了新版本的 Safari。这个新的移动 Safari 支持一些与触摸交互有关的 新事件。后来的 Android 浏览器也实现了同样的事件。当手指放在屏幕上、在屏幕上滑动或从屏幕移开 时,触摸事件即会触发。触摸事件有如下几种。

  • touchstart:手指放到屏幕上时触发(即使有一个手指已经放在了屏幕上)。

  • touchmove:手指在屏幕上滑动时连续触发。在这个事件中调用 preventDefault()可以阻止滚动。

  • touchend:手指从屏幕上移开时触发。

  • touchcancel:系统停止跟踪触摸时触发。文档中并未明确什么情况下停止跟踪。 这些事件都会冒泡,也都可以被取消。尽管触摸事件不属于 DOM 规范,但浏览器仍然以兼容 DOM

的方式实现了它们。因此,每个触摸事件的 event 对象都提供了鼠标事件的公共属性:bubbles、 cancelable、view、clientX、clientY、screenX、screenY、detail、altKey、shiftKey、 ctrlKey 和 metaKey。

除了这些公共的 DOM 属性,触摸事件还提供了以下 3 个属性用于跟踪触点。

  • touches:Touch 对象的数组,表示当前屏幕上的每个触点。

  • targetTouches:Touch 对象的数组,表示特定于事件目标的触点。

  • changedTouches:Touch 对象的数组,表示自上次用户动作之后变化的触点。 每个 Touch 对象都包含下列属性。

  • clientX:触点在视口中的 x 坐标。

  • clientY:触点在视口中的 y 坐标。

  • identifier:触点 ID。

  • pageX:触点在页面上的 x 坐标。

  • pageY:触点在页面上的 y 坐标。

  • screenX:触点在屏幕上的 x 坐标。

  • screenY:触点在屏幕上的 y 坐标。

  • target:触摸事件的事件目标。 这些属性可用于追踪屏幕上的触摸轨迹。例如:

function handleTouchEvent(event) { // 只针对一个触点
  if (event.touches.length == 1) {
    let output = document.getElementById("output");
    switch (event.type) {
      case "touchstart":
        output.innerHTML += `<br>Touch started:` +
          `(${event.touches[0].clientX}` +
          ` ${event.touches[0].clientY})`;
        break;
      case "touchend":
        output.innerHTML += `<br>Touch ended:` +
          `(${event.changedTouches[0].clientX}` +
          ` ${event.changedTouches[0].clientY})`;
        break;
      case "touchmove":
        event.preventDefault(); // 阻止滚动 
        output.innerHTML += `<br>Touch moved:` +
        `(${event.changedTouches[0].clientX}` +
          ` ${event.changedTouches[0].clientY})`;
        break;
    }
  }
}
document.addEventListener("touchstart", handleTouchEvent); 
document.addEventListener("touchend", handleTouchEvent);
document.addEventListener("touchmove", handleTouchEvent);

以上代码会追踪屏幕上的一个触点。为简单起见,代码只会在屏幕有一个触点时输出信息。在 touchstart 事件触发时,触点的位置信息会输出到 output 元素中。在 touchmove 事件触发时,会 取消默认行为以阻止滚动(移动触点通常会滚动页面),并输出变化的触点信息。在 touchend 事件触 发时,会输出触点最后的信息。注意,touchend 事件触发时 touches 集合中什么也没有,这是因为 没有滚动的触点了。此时必须使用 changedTouches 集合。 这些事件会在文档的所有元素上触发,因此可以分别控制页面的不同部分。当手指点触屏幕上的元 素时,依次会发生如下事件(包括鼠标事件): (1) touchstart (2) mouseover (3) mousemove(1 次) (4) mousedown (5) mouseup (6) click (7) touchend

事件委托

  <ul id="myLinks">
      <li id="goSomewhere">Go somewhere</li>
      <li id="doSomething">Do something</li>
      <li id="sayHi">Say hi</li>
</ul>
let list = document.getElementById("myLinks");
list.addEventListener("click", (event) => {
  let target = event.target;
  switch(target.id) {
    case "doSomething":
      document.title = "I changed the document's title";
      break;
    case "goSomewhere":
      location.href = "http:// www.wrox.com";
      break;
    case "sayHi":
      console.log("hi");
      break;
} });

只要可行,就应该考虑只给 document 添加一个事件处理程序,通过它处理页面中所有某种类型的 事件。相对于之前的技术,事件委托具有如下优点。

  • document 对象随时可用,任何时候都可以给它添加事件处理程序(不用等待 DOMContentLoaded 或 load 事件)。这意味着只要页面渲染出可点击的元素,就可以无延迟地起作用。

  • 节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省 DOM 引用,也 24 可以节省时间。

  • 减少整个页面所需的内存,提升整体性能。 最适合使用事件委托的事件包括:click、mousedown、mouseup、keydown 和 keypress。 mouseover 和 mouseout 事件冒泡,但很难适当处理,且经常需要计算元素位置(因为 mouseout 会 在光标从一个元素移动到它的一个后代节点以及移出元素之外时触发)。

删除事件处理程序

 <div id="myDiv">
  <input type="button" value="Click Me" id="myBtn">
</div>
<script type="text/javascript">
  let btn = document.getElementById("myBtn");
  btn.onclick = function() {
// 执行操作
btn.onclick = null; // 删除事件处理程序
document.getElementById("myDiv").innerHTML = "Processing..."; };
</script>

设置<div>元素的 innerHTML 属性之前,按钮的事件处理程序先被删除 了。这样就可以确保内存被回收,按钮也可以安全地从 DOM 中删掉。

自定义 DOM 事件

DOM3 增加了自定义事件的类型。自定义事件不会触发原生 DOM 事件,但可以让开发者定义自己 的事件。要创建自定义事件,需要调用 createEvent("CustomEvent")。返回的对象包含 initCustomEvent()方法,该方法接收以下 4 个参数。

  • type(字符串):要触发的事件类型,如"myevent"。

  • bubbles(布尔值):表示事件是否冒泡。

  • cancelable(布尔值):表示事件是否可以取消。

  • detail(对象):任意值。作为 event 对象的 detail 属性。 自定义事件可以像其他事件一样在 DOM 中派发,比如:

let div = document.getElementById("myDiv"),
    event;
div.addEventListener("myevent", (event) => {
  console.log("DIV: " + event.detail);
});
document.addEventListener("myevent", (event) => {
  console.log("DOCUMENT: " + event.detail);
});
if (document.implementation.hasFeature("CustomEvents", "3.0")) {
   event = document.createEvent("CustomEvent"); 
   event.initCustomEvent("myevent", true, false, "Hello world!");
    div.dispatchEvent(event);
}

Last updated