# 事件流

事件流描述了页面接收事件的顺序。IE 将支持事件冒泡流,而 Netscape Communicator将支持事件捕获流。

# 事件冒泡

IE 事件流被称为事件冒泡,即事件开始时是由具体的元素(文档中嵌套最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档);

# 事件捕获

Netscape Communicator 团队提出了另一种名为事件捕获的事件流。事件捕获的意思是最不具体的节点应该最先收到事件,而最具体的节点应该最后收到事件。 在事件捕获中, click 事件首先由 document 元素捕获,然后沿 DOM 树依次向下传播,直至到达实际的目标元素;

# DOM 事件流

DOM2 Events 规范规定事件流分为 3 个阶段:事件捕获、处于目标和事件冒泡。首先发生的是事件捕获,为截获事件提供了机会,然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。

avatar

在 DOM 事件流中,实际的目标( div 元素)在捕获阶段不会接收到事件。这是因为捕获阶段从document 到 html 再到 body 就结束了。下一阶段,即会在 div 元素上触发事件的“到达目标”阶段,通常在事件处理时被认为是冒泡阶段的一部分。然后,冒泡阶段开始,事件反向传播至文档。大多数支持 DOM事件流的浏览器实现了一个小小的拓展。虽然 DOM2 Events规范明确捕获阶段不命中事件目标,但现代浏览器都会在捕获阶段在事件目标上触发事件。最终结果是在事件目标上有两个机会来处理事件。

# 事件处理程序

事件意味着用户或浏览器执行的某种动作。比如,单击( click )、加载( load )、鼠标悬停( mouseover )。为响应事件而调用的函数被称为事件处理程序(或事件监听器)。事件处理程序的名字以 "on" 开头,因此 click 事件的处理程序叫作 onclick ,而 load 事件的处理程序叫作 onload 。有很多方式可以指定事件处理程序。

# HTML 事件处理程序

特定元素支持的每个事件都可以使用事件处理程序的名字以 HTML 属性的形式来指定。此时属性的值必须是能够执行的 JavaScript 代码。例如,要在按钮被点击时执行某些 JavaScript 代码,可以使用以下 HTML 属性:

<input type="button" value="Click Me" onclick="console.log('Clicked')"/>

注意,因为属性的值是 JavaScript 代码,所以不能在未经转义的情况下使用 HTML 语法字符,比如和号( & )、双引号( " )、小于号( < )和大于号( > )。

这个动态创建的包装函数还有一个特别有意思的地方,就是其作用域链被扩展了。在这个函数中,document 和元素自身的成员都可以被当成局部变量来访问。这是通过使用 with 实现的:

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

这意味着事件处理程序可以更方便地访问自己的属性。下面的代码与前面的示例功能一样:

<!-- 输出"Click Me" -->
<input type="button" value="Click Me" onclick="console.log(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="console.log(username.value)">
  </form>

# DOM0 事件处理程序

在 JavaScript 中指定事件处理程序的传统方式是把一个函数赋值给(DOM 元素的)一个事件处理程序属性。要使用 JavaScript 指定事件处理程序,必须先取得要操作对象的 引用。每个元素(包括 window 和 document )都有通常小写的事件处理程序属性,比如 onclick 。只要把这个属性赋值为一个函数即可:

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

通过将事件处理程序属性的值设置为 null ,可以移除通过 DOM0 方式添加的事件处理程序。

# DOM2 事件处理程序

DOM2 Events 为事件处理程序的赋值和移除定义了两个方法: addEventListener() 和 removeEventListener() 。这两个方法暴露在所有 DOM 节点上,它们接收 3 个参数:事件名、事件处理函数和一个布尔值, true 表示在捕获阶段调用事件处理程序, false (默认值)表示在冒泡阶段调用事件处理程序。

给按钮添加 click 事件处理程序为例,可以这样写:

let btn =  document.getElementById('myBtn')
btn.addEventListener('click', () => {
  console.log(this.id)
}, false)

以上代码为按钮添加了会在事件冒泡阶段触发的 onclick 事件处理程序。

使用 DOM2 方式的主要优势是可以为同一个事件添加多个事件处理程序。多个事件处理程序以添加顺序来触发。

通过 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)


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

大多数情况下,事件处理程序会被添加到事件流的冒泡阶段,主要原因是跨浏览器兼容性好。

# IE 事件处理程序

IE 实现了与 DOM 中类似的两个方法:attachEvent() 和 detachEvent()。这两个方法接收两个同样的参数:事件处理程序的名字和事件处理函数。因为 IE8 及更早版本只支持事件冒泡,所以使用 attachEvent() 添加的事件处理程序会添加到冒泡阶段。

const btn = document.getElementById('myBtn')
btn.attachEvent('onclick', function() {
  console.log('clicked')
})

注意,attachEvent() 的第一个参数是 "onclick" ,而不是 DOM 的 addEventListener() 方法的 "click" 。

在 IE中使用 attachEvent() 与使用 DOM0方式的主要区别是事件处理程序的作用域。使用 DOM0 方式时,事件处理程序中的 this 值等于目标元素。而使用 attachEvent() 时,事件处理程序是在全局作用域中运行的,因此 this 等于 window 。来看下面使用 attachEvent() 的例子:

const btn = doucment.getElementById('myBtn')
btn.attachEvent('onclick', function() {
  console.log(this === window) // true
})

与使用 addEventListener() 一样,使用 attachEvent() 方法也可以给一个元素添加多个事件处理程序。这里的事件处理程序会以添加它们的顺序反向触发。

使用 attachEvent() 添加的事件处理程序将使用 detachEvent() 来移除,只要提供相同的参数。与使用 DOM 方法类似,作为事件处理程序添加的匿名函数也无法移除。但只要传给 detachEvent() 方法相同的函数引用,就可以移除。

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

首先实现一个根据需要分别使用 DOM0 方式、DOM2 方式或 IE 方式来添加事件处理程序。这个方法会在 EventUtil 对象上添加一个方法,以实现跨浏览器事件处理。添加的这个 addHandler() 方法接收 3 个参数:目标元素、事件名和事件处理函数。对应的移除添加事件的方法 removeHandler() 同样接收三个参数。这个方法的任务是移除之前添加的事件处理程序,不管是通过何种方式添加的,默认为 DOM0 方式。

const 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
    }
  }
}

# 事件对象

在出发 DOM 上的某个事件时,会产生一个事件对象 Event,这个对象包含所有与事件相关的信息。这个对象包含了一些基本信息,比如导致事件的元素、发生的事件类型,以及可能与特定事件相关的任何其他数据。

# DOM 中的事件对象

在 DOM 合规的浏览器中, event 对象是传给事件处理程序的唯一参数。不管以哪种方式(DOM0 或 DOM2)指定事件处理程序,都会传入这个 event 对象。下面的例子展示了在两种方式下都可以使用事件对象:

let btn = document.getElementById("myBtn");
btn.onclick = function(event) {
  console.log(event.type); // "click"
};
btn.addEventListener("click", (event) => {
  console.log(event.type); // "click"
}, false);
<input type="button" value="Click Me" onclick="console.log(event.type)">

事件对象包含与特定事件相关的属性和方法。不同的事件生成的事件对象也会包含不同的属性和方法。不过,所有事件对象都会包含下表列出的这些公共属性和方法。

avatar avatar

在事件处理程序内部,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 ,从而触发了在它上面注册的处理程序。

preventDefault() 方法用于阻止特定事件的默认动作。比如,链接的默认行为就是在被单击时导航到 href 属性指定的 URL。如果想阻止这个导航行为,可以在 onclick 事件处理程序中取消,如下面的例子所示:

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

任何可以通过 preventDefault() 取消默认行为的事件,其事件对象的 cancelable 属性都会设置为 true 。

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");
};

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
};

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

# IE 中的事件对象

与 DOM 事件对象不同, IE 事件对象可以基于事件处理程序被指定的方式以不同方式来访问。如果事件处理程序是使用 DOM0 方式指定的,则 event 对象只是 window 对象的一个属性,如下所示:

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

如果事件处理程序是使用 attachEvent() 指定的,则 event 对象会作为唯一的参数传给处理函数,如下所示:

var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(event) {
  console.log(event.type); // "click"
});

使用 attachEvent() 时, event 对象仍然是 window 对象的属性(像 DOM0 方式那样),只是出于方便也将其作为参数传入。

如果是使用 HTML属性方式指定的事件处理程序,则 event 对象同样可以通过变量 event 访问(与 DOM 模型一样)。下面是在 HTML 事件属性中使用 event.type 的例子:

<input type="button" value="Click Me" onclick="console.log(event.type)">

IE 事件对象也包含与导致其创建的特定事件相关的属性和方法,其中很多都与相关的 DOM 属性和方法对应。与 DOM 事件对象一样,基于触发的事件类型不同, event 对象中包含的属性和方法也不一样。不过,所有 IE 事件对象都会包含下表所列的公共属性和方法。

avatar avatar

由于事件处理程序的作用域取决于指定它的方式,因此 this 值并不总是等于事件目标。为此,更好的方式是使用事件对象的 srcElement 属性代替 this 。下面的例子表明,不同事件对象上的 srcElement 属性中保存的都是事件目标:

var btn = document.getElementById("myBtn");
btn.onclick = function() {
  console.log(window.event.srcElement === this); // true
};
btn.attachEvent("onclick", function(event) {
  console.log(event.srcElement === this); // false
});

在第一个以 DOM0 方式指定的事件处理程序中, srcElement 属性等于 this ,而在第二个事件处理程序中(运行在全局作用域下),两个值就不相等了。

returnValue 属性等价于 DOM 的 preventDefault() 方法,都是用于取消给定事件默认的行为。只不过在这里要把 returnValue 设置为 false 才是阻止默认动作。下面是一个设置该属性的例子:

var link = document.getElementById("myLink");
link.onclick = function() {
  window.event.returnValue = false;
};

cancelBubble 属性与 DOM stopPropagation() 方法用途一样,都可以阻止事件冒泡。因为 IE8 及更早版本不支持捕获阶段,所以只会取消冒泡。 stopPropagation() 则既取消捕获也取消冒泡。下面是一个取消冒泡的例子:

var btn = document.getElementById("myBtn");
btn.onclick = function() {
  console.log("Clicked");
  window.event.cancelBubble = true;
};
document.body.onclick = function() {
  console.log("Body clicked");
};

# 跨浏览器事件对象

虽然 DOM 和 IE 的事件对象并不相同,但它们有足够的相似性可以实现跨浏览器方案。

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 = false;
    }
  },
  stopPropagation: function(event) {
    if (event.stopPropagation) {
      event.stopPropagation();
    } else {
      event.cancelBubble = true;
    }
  }
};

使用这些方法的前提是,事件处理程序必须接收 event 对象,并把它传给这个方法。下面是使用 EventUtil 中这个方法统一获取 event 对象的一个例子:

btn.onclick = function(event) {
  event = EventUtil.getEvent(event);
};

# 事件类型

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 元素包含已加载页面的滚动条。
  1. load 事件

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

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

与其他事件一样,事件处理程序会接收到一个 event 对象。这个 event 对象并没有提供关于这种类型事件的额外信息,虽然在 DOM 合规的浏览器中, event.target 会被设置为 document ,但在 IE8 之前的版本中,不会设置这个对象的 srcElement 属性。

片上也会触发 load 事件,包括DOM中的图片和非DOM中的图片。可以在HTML中直接给 img元素的 onload 属性指定事件处理程序,比如:

<img src="smile.gif" onload="console.log('Image loaded.')">

使用 JavaScript也可以为图片指定事件处理程序:

let image = document.getElementById("myImage");
image.addEventListener("load", (event) => {
  console.log(event.target.src);
});

在通过 JavaScript 创建新 img 元素时,也可以给这个元素指定一个在加载完成后执行的事件处理程序。在这里,关键是要在赋值 src 属性前指定事件处理程序,如下所示:

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";
});

同样的技术也适用于 DOM0 的 Image 对象。在 DOM 出现之前,客户端都使用 Image 对象预先加载图片。可以像使用前面(通过 createElement() 方法创建)的 img 元素一样使用 Image 对象,只是不能把后者添加到 DOM 树。下面的例子使用新 Image 对象实现了图片预加载:

window.addEventListener("load", () => {
  let image = new Image();
  image.addEventListener("load", (event) => {
    console.log("Image loaded!");
  });
  image.src = "smile.gif";
});

这里调用 Image 构造函数创建了一个新图片,并给它设置了事件处理程序。有些浏览器会把 Image 对象实现为 img 元素,但并非所有浏览器都如此。所以最好把它们看成是两个东西。

在 IE8 及早期版本中,如果图片没有添加到 DOM 文档中,则 load 事件发生时不会生成 event 对象。对未被添加到文档中的 img 元素以及 Image 对象来说都是这样。 IE9 修复了这个问题。

script 元素会在 JavaScript 文件加载完成后触发 load 事件,从而可以动态检测。下面的代码展示了如何给动态创建的 script 元素指定事件处理程序:

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

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

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

这个事件生成的 event 对象只有 target 属性,值为 document。

HTML 中添加方法:

<!DOCTYPE html>
<html>
  <head>
    <title>Unload Event Example</title>
  </head>
  <body onunload="console.log('Unloaded!')">
  </body>
</html>

因为 unload 事件是在页面卸载完成后触发的,所以不能使用页面加载后才有的对象。此时要访问 DOM 或修改页面外观都会导致错误。

  1. resize 事件

当浏览器窗口被缩放到新高度或宽度时,会触发 resize 事件。这个事件在 window 上触发,因此可以通过 JavaScript 在 window 上或者为 body 元素添加 onresize 属性来指定事件处理程序。

window.addEventListener("resize", (event) => {
  console.log("Resized");
});

生成的 event 对象的 target 属性在 DOM 合规的浏览器中是 document,而 IE8 及更早版本中并没有提供可用的属性。

同浏览器在决定何时触发 resize 事件上存在重要差异。IE、Safari、Chrome 和 Opera 会在窗口缩放超过 1 像素时触发 resize 事件,然后随着用户缩放浏览器窗口不断触发。Firefox 早期版本则只在用户停止缩放浏览器窗口时触发 resize 事件。

  1. scroll 事件

虽然 scroll 事件发生在 window 上,但实际上反映的是页面中相应元素的变化。在混杂模式下,可以通过 body 元素检测 scrollLeft 和 scrollTop 属性的变化。而在标准模式下,所有浏览器中都发生在 html 元素上。下面的代码演示了如何处理这些差异:

window.addEventListener("scroll", (event) => {
  if (document.compatMode == "CSS1Compat") {
    console.log(document.documentElement.scrollTop);
  } else {
    console.log(document.body.scrollTop);
  }
});

# 焦点事件

焦点事件在页面元素获得或失去焦点时触发。这些事件可以与 document.hasFocus() 和 document.activeElement 一起为开发者提供用户在页面中导航的信息。焦点事件有以下 6 种。

  • blur :当元素失去焦点时触发。这个事件不冒泡,所有浏览器都支持。
  • DOMFocusIn :当元素获得焦点时触发。这个事件是 focus 的冒泡版。Opera 是唯一支持这个事件的主流浏览器。DOM3 Events 废弃了 DOMFocusIn ,推荐 focusin 。
  • DOMFocusOut :当元素失去焦点时触发。这个事件是 blur 的通用版。Opera 是唯一支持这个事件的主流浏览器。DOM3 Events废弃了 DOMFocusOut ,推荐 focusout 。
  • focus :当元素获得焦点时触发。这个事件不冒泡,所有浏览器都支持。
  • focusin :当元素获得焦点时触发。这个事件是 focus 的冒泡版。
  • focusout :当元素失去焦点时触发。这个事件是 blur 的通用版

焦点事件中的两个主要事件是 focus 和 blur,它们最大的问题是不冒泡。

当焦点从页面中的一个元素移到另一个元素上时,会依次发生如下事件。

(1) focuscout 在失去焦点的元素上触发。 (2) focusin 在获得焦点的元素上触发。 (3) blur 在失去焦点的元素上触发。 (4) DOMFocusOut 在失去焦点的元素上触发。 (5) focus 在获得焦点的元素上触发。 (6) DOMFocusIn 在获得焦点的元素上触发

其中,blur、DOMFocusOut 和 focusout 的事件目标是失去焦点的元素,而 focus、DOMFocusIn 和 focusin 的事件目标是获得焦点的元素。

# 鼠标和滚轮事件

DOM3 Events 定义了 9 种鼠标事件:

  • click :在用户单击鼠标主键(通常是左键)或按键盘回车键时触发。这主要是基于无障碍的考虑,让键盘和鼠标都可以触发 onclick 事件处理程序。
  • dblclick :在用户双击鼠标主键(通常是左键)时触发。这个事件不是在 DOM2 Events中定义的,但得到了很好的支持,DOM3 Events 将其进行了标准化。
  • mousedown :在用户按下任意鼠标键时触发。这个事件不能通过键盘触发。
  • mouseenter :在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发。 mouseenter 事件不是在 DOM2 Events 中定义的,而是 DOM3 Events 中新增的事件。
  • mouseleave :在用户把鼠标光标从元素内部移到元素外部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发。 mouseleave 事件不是在 DOM2 Events 中定义的,而是 DOM3 Events 中新增的事件。
  • mousemove :在鼠标光标在元素上移动时反复触发。这个事件不能通过键盘触发。
  • mouseout :在用户把鼠标光标从一个元素移到另一个元素上时触发。移到的元素可以是原始元素的外部元素,也可以是原始元素的子元素。这个事件不能通过键盘触发。
  • mouseover :在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不能通过键盘触发。
  • mouseup :在用户释放鼠标键时触发。这个事件不能通过键盘触发。

页面中的所有元素都支持鼠标事件。除了 mouseenter 和 mouseleave ,所有鼠标事件都会冒泡,都可以被取消,而这会影响浏览器的默认行为。

由于事件之间存在关系,因此取消鼠标事件的默认行为也会影响其他事件。比如, click 事件触发的前提是 mousedown 事件触发后,紧接着又在同一个元素上触发了 mouseup事件。如果 mousedown 和 mouseup 中的任意一个事件被取消,那么 click 事件就不会触发。

两次连续的 click 事件会导致 dblclick 事件触发。只要有任何逻辑阻止了这两个 click 事件发生(比如取消其中一个 click 事件或者取消 mousedown 或 mouseup 事件中的任一个), dblclick 事件就不会发生。这 4 个事件永远会按照如下顺序触发:

  • (1) mousedown
  • (2) mouseup
  • (3) click
  • (4) mousedown
  • (5) mouseup
  • (6) click
  • (7) dblclick

鼠标事件在 DOM3 Events 中对应的类型是 "MouseEvent" ,而不是 "MouseEvents" 。鼠标事件还有一个名为滚轮事件的子类别。滚轮事件只有一个事件 mousewheel ,反映的是鼠标滚轮或带滚轮的类似设备上滚轮的交互。

  1. 客户区坐标位置

鼠标事件都是在浏览器视口中的某个位置上发生的。这些信息被保存在 event 对象的 clientX 和clientY 属性中。

下图展示了视口中的客户端坐标; avatar

注意客户端坐标不考虑页面滚动,因此这两个值并不代表鼠标在页面上的位置;只是相对于浏览器窗口的位置。

let div = document.getElementById("myDiv");
div.addEventListener("click", (event) => {
  console.log(`Client coordinates: ${event.clientX}, ${event.clientY}`);
});
  1. 页面坐标位置

页面坐标是事件发生时鼠标光标在页面上的坐标,通过 event 对象的 pageX 和 pageY 可以获取。这两个属性表示鼠标光标在页面上的位置。因此反映的是光标到页面而非视口左边与上边的距离。

let div = document.getElementById("myDiv");
div.addEventListener("click", (event) => {
  console.log(`Page coordinates: ${event.pageX}, ${event.pageY}`);
});
  1. 屏幕坐标位置

可以通过 event 对象的 screenX 和 screenY 属性获取鼠标光标在屏幕上的坐标。

avatar

let div = document.getElementById("myDiv");
div.addEventListener("click", (event) => {
  console.log(`Screen coordinates: ${event.screenX}, ${event.screenY}`);
});
  1. 修饰键

键盘上的修饰键 Shift、Ctrl、Alt 和 Meta 经常用于修改鼠标事件的行为。DOM 规定了 4 个属性来表示这几个修饰键的状态: shiftKey 、 ctrlKey 、 altKey 和 metaKey 。这几属性会在各自对应的修饰键被按下时包含布尔值 true ,没有被按下时包含 false 。在鼠标事件发生的,可以通过这几个属性来检测修饰键是否被按下。

let div = document.getElement('myDiv')
div.addEventListener('click', (event) => {
  let keys = new Array()
  if (event.shiftKey) {
    keys.push('shift')
  }

  if (event.ctrlKey) {
    keys.push('ctrl')
  }

  if (event.altKey) {
    keys.push('alt')
  }

  if(event.metaKey) {
    keys.push('meta')
  }

  console.log(`keys: ${keys.join(',')}`)
})
  1. 相关元素

对 mouseover 和 mouseout 事件而言,还存在与事件相关的其他元素。这两个事件都涉及从一个元素的边界之内把光标移到另一个元素的边界之内。对 mouseover 事件来说,事件的主要目标是获得光标的元素,相关元素是失去光标的元素。类似地,对 mouseout 事件来说,事件的主要目标是失去光标的元素,而相关元素是获得光标的元素。

DOM 通过 event 对象的 relatedTarget 属性提供了相关元素的信息。这个属性只有在 mouseover 和 mouseout 事件发生时才包含值,其他所有事件的这个属性的值都是 null 。对应 IE 中的 fromElement 以及 toElement 属性。

getRelatedTarget: function(event) {
  if (event.relatedTarget) {
    return event.relatedTarget;
  } else if (event.toElement) {
    return event.toElement;
  } else if (event.fromElement) {
    return event.fromElement;
  } else {
    return null;
  }
},
  1. 鼠标按键

对 mousedown 和 mouseup 事件来说, event 对象上会有一个 button 属性,表示按下或释放的是哪个按键。DOM 为这个 button 属性定义了 3 个值:0 表示鼠标左键、1 表示鼠标中键(通常也是滚轮键)、2 表示鼠标右键。

  1. 额外事件信息

DOM2 Events 规范在 event 对象上提供了 detail 属性,以给出关于事件的更多信息。对鼠标事件来说, detail 包含一个数值,表示在给定位置上发生了多少次单击。单击相当于在同一个像素上发生一次 mousedown 紧跟一次 mouseup 。 detail 的值从 1开始,每次单击会加 1。如果鼠标在 mousedown 和 mouseup 之间移动了,则 detail 会重置为 0。

IE 还为每个鼠标事件提供了以下额外信息:

  • altLeft ,布尔值,表示是否按下了左 Alt 键(如果 altLeft 是 true ,那么 altKey 也是 true );
  • ctrlLeft ,布尔值,表示是否按下了左 Ctrl 键(如果 ctrlLeft 是 true ,那么 ctrlKey 也是 true );
  • offsetX ,光标相对于目标元素边界的 x 坐标;
  • offsetY ,光标相对于目标元素边界的 y 坐标;
  • shiftLeft ,布尔值,表示是否按下了左 Shift 键(如果 shiftLeft 是 true ,那么 shiftKey 也是 true )。
  1. mousewheel 事件

mousewheel 事件会在用户使用鼠标滚轮时触发,包括在垂直方向上任意滚动。这个事件会在任何元素上触发,并(在 IE8 中)冒泡到 document 和(在所有现代浏览器中) window 。 mousewheel 事件的 event 对象包含鼠标事件的所有标准信息,此外还有一个名为 wheelDelta 的新属性。当鼠标滚轮向前滚动时,wheelDelta 每次都是 +120;而当鼠标滚轮向后滚动时, wheelDelta 每次都是 –120。(谷歌浏览器是 150)

document.addEventListener("mousewheel", (event) => {
  console.log(event.wheelDelta);
});
  1. 触摸屏设备

在为触摸屏设备开发时,要记住以下事项:

  • 不支持 dblclick 事件。双击浏览器窗口可以放大,但没有办法覆盖这个行为。
  • 单指点触屏幕上的可点击元素会触发 mousemove 事件。如果操作会导致内容变化,则不会再触发其他事件。如果屏幕上没有变化,则会相继触发 mousedown 、 mouseup 和 click 事件。点触不可点击的元素不会触发事件。可点击元素是指点击时有默认动作的元素(如链接)或指定了 onclick 事件处理程序的元素。
  • mousemove 事件也会触发 mouseover 和 mouseout 事件。
  • 双指点触屏幕并滑动导致页面滚动时会触发 mousewheel 和 scroll 事件。

# 键盘与输入事件

键盘事件包含 3 个事件:

  • keydown ,用户按下键盘上某个键时触发,而且持续按住会重复触发。
  • keypress ,用户按下键盘上某个键并产生字符时触发,而且持续按住会重复触发。Esc 键也会触发这个事件。DOM3 Events 废弃了 keypress 事件,而推荐 textInput 事件。
  • keyup ,用户释放键盘上某个键时触发

在文本框中输入时,会触发 textInput 事件。这个事件是对 keypress 事件的扩展,用于在文本显示给用户之前更方便地截获文本输入。textInput 会在文本被插入到文本框之前触发。

当用户按下键盘上的某个字符键时,首先会触发 keydown 事件,然后触发 keypress 事件,最后触发 keyup 事件。注意,这里 keydown 和 keypress 事件会在文本框出现变化之前触发,而 keyup 事件会在文本框出现变化之后触发。如果一个字符键被按住不放, keydown 和 keypress 就会重复触发,直到这个键被释放。

键盘事件支持与鼠标事件相同的修饰键。 shiftKey 、 ctrlKey 、 altKey 和 metaKey 属性在键盘事件中都是可用的。

  1. 键码

对于 keydown 和 keyup 事件, event 对象的 keyCode 属性中会保存一个键码,对应键盘上特定的一个键。对于字母和数字键, keyCode 的值与小写字母和数字的 ASCII 编码一致。比如数字 7 键的 keyCode 为 55,而字母 A 键的 keyCode 为 65,而且跟是否按了 Shift 键无关。

let textbox = document.getElementById('myText')
textbox.addEventListener('keyup', (event) => {
  console.log(event.keycode)
})

下面是键盘上所有非字符键的键码:

avatar

  1. 字符编码

在 keypress 事件发生时,意味着按键会影响屏幕上显示的文本。对插入或移除字符的键,所有浏览器都会触发 keypress 事件,其他键则取决于浏览器。浏览器在 event 对象上支持 charCode 属性,只有发生 keypress 事件时这个属性才会被设置值,包含的是按键字符对应的 ASCII 编码。通常, charCode 属性的值是 0,在 keypress 事件发生时则是对应按键的键码。要以跨浏览器方式获取字符编码,首先要检查 charCode 属性是否有值,如果没有再使用 keyCode ,如下所示:

const getCharCode = (event) => {
  if (typeof event.charCode === 'number') {
    return event.charCode
  } else {
    return event.keyCode
  }
}

let textbox = document.getElementById("myText");
textbox.addEventListener("keypress", (event) => {
  console.log(getCharCode(event));
});
  1. DOM3 的变化

DOM3 Events 规范并未规定 charCode 属性,而是定义了 key 和 char 两个新属性。其中, key 属性用于替代 keyCode ,且包含字符串。在按下字符键时, key 的值等于文本字符(如“k”或“M”);在按下非字符键时, key 的值是键名(如“Shift”或“ArrowDown”)。 char 属性在按下字符键时与 key 类似,在按下非字符键时为 null 。

let textbox = document.getElementById("myText");
textbox.addEventListener("keypress", (event) => {
  let identifier = event.key || event.keyIdentifier;
  if (identifier) {
    console.log(identifier);
  }
});

由于缺乏跨浏览器支持,因此不建议使用 key 、 keyIdentifier 、和 char 。

DOM3 Events 也支持一个名为 location 的属性,该属性是一个数值,表示是在哪里按的键。

  1. textInput 事件

DOM3 Events 规范增加了一个名为 textInput 的事件,其在字符被输入到可编辑区域时触发。keypress 和 textInput 之间的区别是,keypress 会在任何可以获得焦点的元素上触发,而 textInput 只在可编辑区域上触发。另一个区别是 textInput 只在有新字符被插入时才会触发,而 keypress 对任何可能影响文本的键都会触发(包括退格键)。

因为 textInput 事件主要关注字符,所以在 event 对象上提供了一个 data 属性,包含要插入的字符(不是字符编码)。 data 的值始终是要被插入的字符,因此如果在按 S 键时没有按 Shift键, data 的值就是 "s" ,但在按 S 键时同时按 Shift 键, data 的值则是 "S" 。

let textbox = document.getElementById("myText");
textbox.addEventListener("textInput", (event) => {
  console.log(event.data);
});

event 对象上还有一个名为 inputMethod 的属性,该属性表示向控件中输入文本的手段。可能的值如下:

  • 0,表示浏览器不能确定是什么输入手段;
  • 1,表示键盘;
  • 2,表示粘贴;
  • 3,表示拖放操作;
  • 4,表示 IME;
  • 5,表示表单选项;
  • 6,表示手写(如使用手写笔);
  • 7,表示语音;
  • 8,表示组合方式;
  • 9,表示脚本。

# HTML5 事件

  1. contextmenu 事件

由于 contextmenu 事件冒泡,因此只要给 document 指定一个事件处理程序就可以处理页面上的所有同类事件。事件目标是触发操作的元素。这个事件在所有浏览器中都可以取消,在 DOM 合规的浏览器中使用 event.preventDefault() 。contextmenu 事件应该算一种鼠标事件,因此 event 对象上的很多属性都与光标位置有关。

下面的代码是实现一个右击菜单:

<!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>
  </body>
</html>

ul 元素开始时是隐藏的;下面是 js 代码:

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'
  })
})
  1. beforeunload 事件

这个事件会在当要关闭浏览器窗口的时候触发。需要将 event.returnValue 设置为要在确认框中显示的字符串(对于 IE 和 Firefox 来说),并将其作为函数值返回(对于 Safari 和 Chrome 来说),如下所示:

window.addEventListener("beforeunload", (event) => {
  let message = "I'm really going to miss you if you go.";
  event.returnValue = message;
  return message;
});
  1. DOMContentLoaded 事件

window 的 load 事件会在页面完全加载后触发,因为要等待很多外部资源加载完成,所以会花费较长时间。而 DOMContentLoaded 事件会在 DOM 树构建完成后立即触发,而不用等待图片、JavaScript 文件、CSS 文件或其他资源加载完成。相对于 load 事件, DOMContentLoaded 可以让开发者在外部资源下载的同时就能指定事件处理程序,从而让用户能够更快地与页面交互。

要处理 DOMContentLoaded 事件,需要给 document 或 window 添加事件处理程序(实际的事件目标是 document ,但会冒泡到 window )。下面是一个在 document 上监听 DOMContentLoaded 事件的例子:

document.addEventListenrt('DOMContentLoaded', (event) => {
  console.log('content loaded')
})

DOMContentLoaded 事件通常用于添加事件处理程序或执行其他 DOM 操作。这个事件始终在 load 事件之前触发。

对于不支持 DOMContentLoaded 事件的浏览器,可以使用超时为 0 的 setTimeout() 函数,通过其回调来设置事件处理程序,比如:

setTimeout(() => {
  // 添加事件处理程序
}, 0)

以上代码本质上意味着在当前 JavaScript 进程执行完毕后立即执行这个回调。页面加载和构建期间,只有一个 JavaScript 进程运行。所以可以在这个进程空闲后立即执行回调,至于是否与同一个浏览器或同一页面上不同脚本的 DOMContentLoaded 触发时机一致并无绝对把握。为了尽可能早一些执行,以上代码最好是页面上的第一个超时代码。

  1. hashchange 事件

HTML5 增加了 hashchange 事件,用于在 URL 散列值(URL 最后 # 后面的部分)发生变化时通知开发者。

onhashchange 事件处理程序必须添加给 window ,每次 URL 散列值发生变化时会调用它。 event 对象有两个新属性:oldURL 和 newURL。这两个属性分别保存变化前后的 URL,而且是包含散列值的完整 URL。下面的例子展示了如何获取变化前后的 URL:

window.addEventListener("hashchange", (event) => {
  console.log(`Old URL: ${event.oldURL}, New URL: ${event.newURL}`)
})

// 如果想要获取当前的哈希值,可以使用 location 对象
window.addEventListener('hanshchange', (event) => {
  console.log(`Current hash: ${location.hash}`)
})

# 触摸及手势事件

  1. 触摸事件

当手指放在屏幕上、在屏幕上滑动或从屏幕移开时,触摸事件即会触发。触摸事件有如下几种。

  • touchstart :手指放到屏幕上时触发(即使有一个手指已经放在了屏幕上)。
  • touchmove :手指在屏幕上滑动时连续触发。在这个事件中调用 preventDefault() 可以阻止滚动。
  • touchstart :手指放到屏幕上时触发(即使有一个手指已经放在了屏幕上)。
  • touchmove :手指在屏幕上滑动时连续触发。在这个事件中调用 preventDefault() 可以阻止滚动。

这些事件都会冒泡,也都可以被取消。尽管触摸事件不属于 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);

注意, touchend 事件触发时 touches 集合中什么也没有,这是因为没有滚动的触点了。此时必须使用 changedTouches 集合。

这些事件会在文档的所有元素上触发,因此可以分别控制页面的不同部分。当手指点触屏幕上的元素时,依次会发生如下事件(包括鼠标事件):

  • (1) touchstart
  • (2) mouseover
  • (3) mousemove (1 次)
  • (4) mousedown
  • (5) mouseup
  • (6) click
  • (7) touchend
  1. 手势事件

手势事件会在两个手指触碰屏幕且相对距离或旋转角度变化时触发。手势事件有以下 3 种。

  • gesturestart :一个手指已经放在屏幕上,再把另一个手指放到屏幕上时触发。
  • gesturechange :任何一个手指在屏幕上的位置发生变化时触发。
  • gestureend :其中一个手指离开屏幕时触发。

只有在两个手指同时接触事件接收者时,这些事件才会触发。在一个元素上设置事件处理程序,意味着两个手指必须都在元素边界以内才能触发手势事件(这个元素就是事件目标)。因为这些事件会冒泡,所以也可以把事件处理程序放到文档级别,从而可以处理所有手势事件。使用这种方式时,事件的目标就是两个手指均位于其边界内的元素。

触摸事件和手势事件存在一定的关系。当一个手指放在屏幕上时,会触发 touchstart 事件。当另一个手指放到屏幕上时, gesturestart 事件会首先触发,然后紧接着触发这个手指的 touchstart 事件。如果两个手指或其中一个手指移动,则会触发 gesturechange 事件。只要其中一个手指离开屏幕,就会触发 gestureend 事件,紧接着触发该手指的 touchend 事件。

与触摸事件类似,每个手势事件的 event 对象都包含所有标准的鼠标事件属性: bubbles 、cancelable 、 view 、 clientX 、 clientY 、 screenX 、 screenY 、 detail 、 altKey 、 shiftKey 、ctrlKey 和 metaKey 。新增的两个 event 对象属性是 rotation 和 scale 。 rotation 属性表示手指变化旋转的度数,负值表示逆时针旋转,正值表示顺时针旋转(从 0 开始)。 scale 属性表示两指之间距离变化(对捏)的程度。开始时为 1,然后随着距离增大或缩小相应地增大或缩小。 可以像下面这样使用手势事件的属性:

function handleGestureEvent(event) {
  let output = document.getElementById("output");
  switch(event.type) {
    case "gesturestart":
      output.innerHTML += `Gesture started: ` + `rotation=${event.rotation},` + `scale=${event.scale}`;
      break;
    case "gestureend":
      output.innerHTML += `Gesture ended: ` + `rotation=${event.rotation},` + `scale=${event.scale}`;
      break;
    case "gesturechange":
      output.innerHTML += `Gesture changed: ` + `rotation=${event.rotation},` + `scale=${event.scale}`;
      break;
  }
}
document.addEventListener("gesturestart", handleGestureEvent, false);
document.addEventListener("gestureend", handleGestureEvent, false);
document.addEventListener("gesturechange", handleGestureEvent, false);

# 内存与性能

在 JavaScript 中,页面中事件处理程序的数量与页面整体性能直接相关。因为每个函数都是对象,都占用内存空间,对象越多,性能越差。其次,为指定事件处理程序所需访问 DOM 的次数会先期造成整个页面交互的延迟。

# 事件委托

事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件。例如,click 事件冒泡到 document。这意味着可以为整个页面指定一个 onclick 事件处理程序,而不用为每个可点击元素分别指定事件处理程序。比如有以下 HTML:

<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 对象随时可用,任何时候都可以给它添加事件处理程序(不用等待 DOMContentLoaded 或 load 事件)。这意味着只要页面渲染出可点击的元素,就可以无延迟地起作用。
  • 节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省 DOM引用,也可以节省时间。
  • 减少整个页面所需的内存,提升整体性能。

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

# 删除事件处理程序

把事件处理程序指定给元素后,在浏览器代码和负责页面交互的 JavaScript 代码之间就建立了联系。这种联系建立得越多,页面性能就越差。除了通过事件委托来限制这种连接之外,还应该及时删除不用的事件处理程序。

导致这个问题的原因主要有两个。第一个是删除带有事件处理程序的元素。比如通过真正的 DOM 方法 removeChild() 或 replaceChild() 删除节点。最常见的还是使用 innerHTML 整体替换页面的某一部分。这时候,被 innerHTML 删除的元素上如果有事件处理程序,就不会被垃圾收集程序正常清理。 比如:

<div id="myDiv">
  <input type="button" value="Click Me" id="myBtn">
</div>
<script type="text/javascript">
  let btn = document.getElementById("myBtn");
  btn.onclick = function() {
    // 执行操作
    document.getElementById("myDiv").innerHTML = "Processing...";
    // 不好!
  };
</script>

如果知道某个元素会被删除,那么最好在删除它之前手工删除它的事件处理程序,比如:

<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>

另一个可能导致内存中残留引用的问题是页面卸载。一般来说,最好在 onunload 事件处理程序中趁页面尚未卸载先删除所有事件处理程序。这时候也能体现使用事件委托的优势,因为事件处理程序很少,所以很容易记住要删除哪些。关于卸载页面时的清理,可以记住一点:onload 事件处理程序中做了什么,最好在 onunload 事件处理程序中恢复。

# 模拟事件

# DOM 事件模拟

可以使用 document.createEvent() 方法创建一个 event 对象。这个方法接收一个参数,此参数是一个表示要创建事件类型的字符串。在 DOM2 中,所有这些字符串都是英文复数形式,但在 DOM3 中,又把它们改成了英文单数形式。可用的字符串值是以下值之一。

  • "UIEvents" (DOM3 中是 "UIEvent" ):通用用户界面事件(鼠标事件和键盘事件都继承自这个事件)。
  • "MouseEvents" (DOM3 中是 "MouseEvent" ):通用鼠标事件。
  • "HTMLEvents" (DOM3 中没有):通用 HTML 事件(HTML 事件已经分散到了其他事件大类中)。

创建 event 对象之后,需要使用事件相关的信息来初始化。每种类型的 event 对象都有特定的方法,可以使用相应数据来完成初始化。方法的名字并不相同,这取决于调用 createEvent() 时传入的参数。

事件模拟的最后一步是触发事件。为此要使用 dispatchEvent() 方法,这个方法存在于所有支持事件的 DOM 节点之上。dispatchEvent() 方法接收一个参数,即表示要触发事件的 event 对象。调用 dispatchEvent() 方法之后,事件就“转正”了,接着便冒泡并触发事件处理程序执行。

  1. 模拟鼠标事件

模拟鼠标事件需要先创建一个新的鼠标 event 对象,然后再使用必要的信息对其进行初始化。要创建鼠标 event 对象,可以调用 createEvent() 方法并传入 "MouseEvents" 参数。这样就会返回一个 event 对象,这个对象有一个 initMouseEvent() 方法,用于为新对象指定鼠标的特定信息。initMouseEvent() 方法接收 15 个参数,分别对应鼠标事件会暴露的属性。这些参数列举如下。

  • type (字符串):要触发的事件类型,如 "click" 。
  • bubbles (布尔值):表示事件是否冒泡。为精确模拟鼠标事件,应该设置为 true 。
  • cancelable (布尔值):表示事件是否可以取消。为精确模拟鼠标事件,应该设置为 true 。
  • view (AbstractView):与事件关联的视图。基本上始终是 document.defaultView 。
  • detail (整数):关于事件的额外信息。只被事件处理程序使用,通常为 0。
  • screenX (整数):事件相对于屏幕的 x 坐标。
  • screenY (整数):事件相对于屏幕的 y 坐标。
  • clientX (整数):事件相对于视口的 x 坐标。
  • clientY (整数):事件相对于视口的 y 坐标。
  • ctrlkey (布尔值):表示是否按下了 Ctrl 键。默认为 false 。
  • altkey (布尔值):表示是否按下了 Alt 键。默认为 false 。
  • shiftkey (布尔值):表示是否按下了 Shift 键。默认为 false 。
  • metakey (布尔值):表示是否按下了 Meta 键。默认为 false 。
  • button (整数):表示按下了哪个按钮。默认为 0。
  • relatedTarget (对象):与事件相关的对象。只在模拟 mouseover 和 mouseout 时使用。

initMouseEvent() 方法的这些参数与鼠标事件的 event 对象属性是一一对应的。前 4 个参数是正确模拟事件唯一重要的几个参数,这是因为它们是浏览器要用的,其他参数则是事件处理程序要用的。 event 对象的 target 属性会自动设置为调用 dispatchEvent() 方法时传入的节点。下面来看一个使用默认值模拟单击事件的例子:

let btn = document.getElementById('myBtn')

// 创建 event 对象
let event = document.createEvent('MouseEvents')

// 初始化 event 对象
event.initMouseEvent('click', true, true, document.defaultView, 0, 0, 0, 0, 0, false, false, false, 0, null)

// 出发事件
btn.dispatchEvent(event)
  1. 自定义 DOM 事件

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

  • type (字符串):要触发的事件类型,如 "myevent" 。
  • bubbles (布尔值):表示事件是否冒泡。
  • cancelable (布尔值):表示事件是否可以取消。
  • detail (对象):任意值。作为 event 对象的 detail 属性。 自定义事件可以像其他事件一样在 DOM 中派发,比如:
let div = document.getElementById("myDiv"),

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);
}

评 论:

更新: 12/27/2020, 4:59:16 PM