Skip to main content

DOM事件基本概念

DOM 事件简介

EventTarget 接口定义了与事件相关的方法和属性, Node 接口继承了 EventTarget,因此所有的 DOM 对象都可以使用 EventTarget 中的方法和属性。

为事件绑定事件处理程序的方式

当某个 DOM 触发事件时,我们可以为其绑定一个 handler,即该事件发生时执行的函数。

为 DOM 事件绑定 handler 有如下三种方式。

标签内联绑定

<button onclick='alert("hello!")'>Click me</button>

直接在标签内添加上 on<event> 这样的 attribute,当该 DOM 事件触发时,就会执行后面的代码了。

important

html 标签的属性名对大小写不敏感,因此这里无论是 onclick 还是 onClick 都一样。

通过 DOM 对象属性

<button id="btn">Click me</button>
<script>
btn.onclick = function() {
alert("hello!");
}
</script>

我们还可以通过 elem.on<event> 来对其绑定事件处理函数,当事件触发时就会执行该函数了。

important

关于事件处理函数中的 this 指向的实际上是触发该事件的 DOM 对象本身,所以,某个 DOM 触发事件时,会由该 DOM 对象本身去调用其事件处理函数。

elem.addEventListener

上面的方式都有一个缺点就是,我们不能给同一个 DOM 事件绑定多个处理函数。

btn.onclick = function() { alert(1); }
// ...
btn.onclick = function() { alert(2); } // 替换了前一个处理程序

使用 elem.addEventListener 可以解决这个问题,它的作用是给一个 DOM 对象绑定一个 事件监听器,如果对应事件触发就会执行事件处理函数。

element.addEventListener(event, handler[, options]);
  • event 事件名,比如 click
  • handler 事件处理函数
  • options 具有以下属性的附加可选对象:
    • once: boolean,如果为 true 那么该监听器只会监听一次该事件,事件触发后自动删除监听器
    • capture: boolean,用于设置事件处理阶段,和 事件的冒泡与捕获 有关
    • passive: boolean,如果为 true,那么处理函数将不会调用 preventDefault(),和 浏览器默认行为 有关。
important

elem.addEventListeneroption 也可以是一个 布尔类型,其等价于 option: { capture: true/false }

你可以使用该方法给同一个 DOM 对象的同一个事件绑定多个事件处理函数。

btn.addEventListener("click", function() {
alert("click 1");
});

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

btn.addEventListener("click", function() {
alert("click 3");
});

事件处理函数的执行顺序就是监听器的绑定顺序。

important

对于某些事件,你只能通过 addEventListener 设置事件处理函数。

比如,DOMContentLoaded 事件,该事件会在 DOM 树构建完毕时执行。

// 无效
document.onDOMContentLoaded = function() {
alert("DOM built");
};

// 这种方式可以运行
document.addEventListener("DOMContentLoaded", function() {
alert("DOM built");
});

如果你想为 DOM 对象移除一个事件监听器,那么可以使用 elem.removeEventListener

element.removeEventListener(event, handler[, options]);

需要注意的是,你必须传入同一个 event 及其 handler

事件对象

当一个事件触发时,浏览器还会创建一个 事件对象,并且会将其传入到事件处理函数中,其记录了事件触发时的信息。

  • 鼠标指针的坐标
  • 键盘按下了哪个键
  • 哪个 DOM 对象触发的该事件
  • ...

触发不同的事件,会创建不同类型的 事件对象,其记录的信息也不一样,如果触发一个点击事件,那么该事件对象就是 PointerEvent 类型的事件,下面是它的一些属性。

  • event.type,表示该事件的类型,比如 "click"
  • event.target,表示触发该事件的 DOM 对象
  • event.clientX / event.clientY,指针相对于窗口的坐标

事件处理对象

前面我们通过 addEventListener 来给某个 DOM 对象添加监听器时,都是传入的一个 事件处理函数,实际上我们还可以传入一个 事件处理对象,当事件触发时,该对象会调用其 handleEvent 方法并传入事件对象(必须实现该方法)。

class Menu {
handleEvent(event) {
switch(event.type) {
case 'mousedown':
elem.innerHTML = "Mouse button pressed";
break;
case 'mouseup':
elem.innerHTML += "...and released.";
break;
}
}
}

let menu = new Menu();
elem.addEventListener('mousedown', menu);
elem.addEventListener('mouseup', menu);

事件的冒泡

事件的冒泡指的就是,当一个 DOM 对象触发事件时,会先执行该 DOM 对象的事件处理函数,然后执行行其父元素上的处理函数,一直向上到其他祖先上的处理函数。

直接看下面这个例子。

[window, document, document.documentElement, document.body, btn].forEach((elem) => {
elem.addEventListener("click", (e) => {
alert((e.currentTarget.nodeName || "window") + " is " + "clicked");
});
})

这里依次给 DOM 树根节点、html 元素、body 元素和 btn 元素绑定了点击事件的处理函数,事件处理函数中的 e.currentTarget 表示的是调用该事件处理函数的对象,等价于 this

当我们点击页面中的按钮后,就会依次输出如下内容:

  • "BUTTON is clicked"
  • BODY is clicked
  • "HTML is clicked"
  • #document is clicked
  • window is clicked

如果我们不点击按钮,而是点击页面中的其他地方(body),就会依次输出如下内容:

  • BODY is clicked
  • "HTML is clicked"
  • #document is clicked
  • window is clicked

由此可见,某个 DOM 对象触发事件时,事件处理函数会从该 DOM 对象、其父元素、祖先元素 ... 直至 window 对象依次执行。

需要注意,并不是所有事件都会冒泡,比如 focus 事件就不会进行冒泡。

e.currentTargete.target 的区别

在事件处理函数中,前者等价于 this,表示调用此次事件处理函数的对象,后者表示真正触发该事件的 DOM 对象,在冒泡过程中不会改变。

如果我们将上面例子中的 e.currentTarget 修改为 e.target,点击按钮后输出的都是 BUTTON is clicked,因为事件触发的源头是来自 btn

事件的捕获

DOM 事件标准描述了事件传播的 3 个阶段:

  • 捕获阶段 —— 事件会从 window/document 向下走近触发事件的 DOM 对象
  • 目标阶段 —— 找到触发事件的目标 DOM 对象后,此时目标 DOM 对象开始触发事件
  • 冒泡阶段 —— 目标 DOM 对象触发完事件后,事件开始向父元素冒泡,依次触发父元素、祖先元素上对应事件直到 window/document

下面是在表格中点击 <td> 的图片,摘自规范:

pic.nm

当我们点击页面中表格的某个单元格时,事件传播会先进入捕获阶段,事件会从 window 向下传递目的是找到触发事件的元素(不会执行事件处理函数),达到目标元素后,进入目标阶段,目标元素会触发该事件,然后进入冒泡阶段,按照捕获阶段的路径,向上传递事件,经过的元素都会依次执行该事件的处理函数。

之前提到 addEventListener 有第三个参数,可以传入一个布尔类型等价于传入 { captrue: true/false },它的作用就是让该 DOM 对象的事件处理函数在捕获阶段就触发。

还是上面的那个例子,我们修改如下代码:

<script>
const wrapper = document.getElementById("wrapper");
const container = document.getElementById("container");
const elem = document.getElementById("element");

wrapper.addEventListener("click",(e) => {
window.alert(e.currentTarget.id);
}, true);

container.addEventListener("click",(e) => {
window.alert(e.currentTarget.id);
});

elem.addEventListener("click",(e) => {
window.alert(e.currentTarget.id);
}, true);
</script>

当点击 Element 后,会进入事件的捕获阶段,由于 Wrapper 绑定的事件处理函数是在捕获阶段执行,所以当事件到达 Wrapper 就会执行事件处理函数。

接着事件到达 Container,它的事件处理函数是在冒泡阶段才执行,所以捕获阶段不会执行。

然后事件达到 Element,它的事件处理函数也会在捕获阶段执行,然后进入目标阶段,再到冒泡阶段,到达 Container 后执行其事件处理函数。

整个过程如下:

  • 捕获阶段
    • ...
    • 事件到达 Wrapper 执行其事件处理函数
    • 事件达到 Container 不会执行事件处理函数
    • 事件达到 Element 执行其事件处理函数
  • 目标阶段
    • Element 不会执行其事件处理函数
  • 冒泡阶段
    • 事件到达 Container 执行其事件处理函数
    • 事件达到 Wrapper 不会执行其事件处理函数
    • ...
移除事件监听器要在事件传播同一阶段

之前提到调用 removeEventListener 时,需要传入相同的事件和同引用的事件处理函数,实际上它还需要在事件同一阶段。

比如通过 btn.addEventListener("click", handleClick, true) 在事件捕获阶段为 btn 绑定了一个监听器,如果要移除它,则需要使用 btn.removeEventListener("click", handleClick, true)

阻止事件传播

冒泡事件从目标元素开始向上传播。通常,它会一直上升到 <html>,然后再到 document 对象,有些事件甚至会到达 window,它们会调用路径上所有的处理函数。

在某个事件处理函数中调用 event.stopPropagation(),就可以阻止将当前触发的事件传播到上层。

<body>
<div id="wrapper">Wrapper
<div id="container">Container
<div id="element">Element</div>
</div>
</div>
</body>

<script>
const wrapper = document.getElementById("wrapper");
wrapper.addEventListener("click", function (e) {
alert(e.target.id);
});
</script>

pic.sm

当点击 Element 时,事件会冒泡到 Wrapper,最终打印的 e.target.id 就是 "wrapper"

<script>
const wrapper = document.getElementById("wrapper");
const elem = document.getElementById("element");

wrapper.addEventListener("click", function (e) {
alert(e.target.id);
});

elem.addEventListener("click", function (e) {
e.stopPropagation(); // 阻止冒泡
});
</script>

如果我们在 Element 的事件处理函数中调用了 event.stopPropagation(),那么事件从当前 DOM 元素就不会向上传播了,表现出的行为就是,如果我们点击 Element 则不会打印任何数据,因为没有了事件冒泡后 Wrapper 的点击事件并不会触发。

同理,如果我们在 事件捕获阶段的监听器的事件处理函数 中使用 evnet.stopPropagation,那么也可以阻止事件在捕获阶段向下传播。

event.stopImmediatePropagation()

我们可以使用 addEventListener 为同一个 DOM 对象的同一个事件绑定多个事件监听器。当在某个事件处理函数中调用 event.stopImmediatePropagation() 后,同一个事件剩余监听器上的事件处理函数就不会再被调用,并且该方法也可以阻止事件传播。

事件的委托

事件的委托是一种事件处理模式,它的本质利用的就是事件的捕获和冒泡机制。

对于一个表格,如果里面有许多的单元格,并且我们要在单元格被点击后让其背景变色,通常情况下我们需要给里面的每一个单元格都绑定点击事件的监听器,这非常麻烦,如果使用事件的委托,那么只需要给表格绑定绑定一个点击事件的监听器即可。

下面来看一个例子。

const table = document.querySelector("table");

table.addEventListener("click", function(e) {
const targetEle = e.target;
if (targetEle.nodeName === "TD") {
targetEle.style.backgroundColor = "skyblue";
}
});

这里我们只给最外层的 table 绑定了一个点击事件,利用事件的冒泡机制,如果我们点击了某个单元格,最终事件也会向上传播到 table 上,从而触发 table 的事件处理函数,然后在事件处理函数中使用 e.target 来对真正触发事件的 DOM 元素进行判断即可。

浏览器默认行为

许多事件会自动触发浏览器执行某些行为。

例如:

  • 点击一个链接 —— 触发导航(navigation)到该 URL
  • 点击表单的提交按钮 —— 触发提交到服务器的行为
  • 在文本上按下鼠标按钮并移动 —— 选中文本
  • 点击鼠标右键,会打开浏览器默认的上下文菜单

阻止浏览器的行为

在事件处理函数中调用 event.preventDefault() 方法,就可以阻止该事件触发后,浏览器的默认行为。

<a href="https://www.baidu.com" onclick="event.preventDefault()">here</a>

这样点击这个链接后,就无法进行链接跳转了。

还有一种方式就是对于 on<event> 绑定的事件,我们需要在事件处理函数中返回 false

<a href="https://www.baidu.com" onclick="return false">here</a>
<a id="link" href="https://www.baidu.com">here</a>

<script>
document.getElementById("link").onclick = function(e) {
return false;
};
</script>
前置事件

某些事件的触发会在某个前置事件触发后自动触发,比如 focus 事件的前置事件可以是 mousedown 事件,对于一个表单元素,如果你在 mousedown 事件的事件处理函数中阻止了浏览器行为,那么 focus 事件就不会自动触发了。

passive 选项

之前提到过 addEventListener 的第三个可选参数中,传入的对象还可以有 passive 属性,它是一个布尔值,如果为 true 则会忽略事件处理函数中 event.preventDefault() 的调用。

大部分的事件的 passive 参数默认为 false,但是对于 Firefox 和 Chrome 浏览器,touchstarttouchmove 事件的 passvie 默认为 true。

event.defaultPrevented 属性

在执行了 event.preventDefault() 之后,该属性就会变成 true

创建自定义事件