目录

🥘 JavaScript 浏览器事件

# 浏览器事件简介

DOM 事件是在某个时刻触发的信号。

常见的事件列表

  • 鼠标事件

    • click :鼠标点击一个元素时(或者触摸屏点击元素)。
    • contextmenu :当鼠标右键点击一个元素时。
    • mouseover / mouseout : 当鼠标指针移入 / 离开一个元素时。
    • mousedown / mouseup :当在元素上按下 / 释放鼠标按钮时。
    • mousemove :当鼠标移动时。
  • 键盘事件

    • keydownkeyup :当按下和松开一个按键时。
  • 表单事件:

    • submit :当访提交了一个 <form> 时。
    • focus :当访问者聚焦于一个表单元素时,例如聚焦于一个 <input>
  • 文档事件:

    • DOMContentLoaded :当 HTML 的加载和处理均完成,DOM 被完全构建完成时。
  • CSS 事件:

    • transitionend :当一个 CSS 动画完成时。

# 事件处理

要对事件响应,可以分配一个处理事件的程序:一个事件发生时运行的函数

# 对于 HTML 特性

🌰 例子 / 点击按钮事件:

<input value="Click me" onclick="alert('Click!')" type="button">
1

🌰 例子 / 事件处理函数:

<script>
  function countRabbits() {
    for(let i=1; i<=3; i++) {
      alert("Rabbit number " + i);
    }
  }
</script>

<input type="button" onclick="countRabbits()" value="Count rabbits!">
1
2
3
4
5
6
7
8
9

由于 HTML 中标签特性 大小写不敏感,所以事件特性命名可以随意,但是一般小写,例如 onclick

# 对于 DOM 属性

使用 DOM 属性 on<event> 分配事件处理程序。

🌰 例子:

<input id="elem" type="button" value="Click me">
<script>
  let elem = document.querySelector("#elem")
  elem.onclick = function() {
    alert('Thank you');
  };
</script>
1
2
3
4
5
6
7

如果一个处理程序是通过 HTML 特性分配的,那么随后浏览器读取它,并从特性的内容创建一个新函数,并将这个函数写入 DOM 属性。

注意

注意 DOM 属性是大小写敏感的。

初始化的顺序:

🌰 例子:

<input type="button" id="elem" onclick="alert('Before')" value="Click me">
<script>
  elem.onclick = function() { // 覆盖了现有的处理程序
    alert('After'); // 只会显示此内容
  };
</script>
1
2
3
4
5
6

这个示例中,使用 JavaScript 添加了一个处理程序,覆盖了现有的处理程序。

所以如果要溢出这个处理程序,赋值 elem.onclick = null

# 元素 this

处理程序中的 this 的值是对应的元素。就是处理程序所在的那个元素。

🌰 例子:

<button onclick="alert(this.innerHTML)">Click me</button>
1

::: demo[vanilla]

<button onclick="alert(this.innerHTML)">Click me</button>
1

:::

# 注意问题

  • 在脚本中,使用现存的函数作为事件的处理程序时,赋值时应该使用 函数名不带 () 的形式。

    🌰 例子:

    // 正确
    button.onclick = sayThanks;
    
    // 错误
    button.onclick = sayThanks();
    
    1
    2
    3
    4
    5

    如果添加了 () ,那么就变成了 函数的调用,赋值的是 函数运行之后的结果(没有返回值则为 undefined,并没有赋值任何的事件处理 handler

  • 在 HTML 特性中,分配事件处理程序需要加 ()

    🌰 例子:

    <input type="button" id="button" onclick="sayThanks()">
    
    1

    因为给特性赋值,相当于给 属性赋值:

    button.onclick = function() {
      sayThanks(); // <-- 特性(attribute)中的内容变到了这里
    };
    
    1
    2
    3

    所以这里需要 事件处理程序的执行。

  • 不要对处理程序使用 setAttribute

    🌰 例子:

    document.body.setAttribute('onclick', function() { alert(1) }); // 无效
    
    1

# 事件监听 addEventListener

要为事件 分配多个处理程序时,使用 addEventListenerremoveEventListener 管理处理程序。

  • 对元素添加处理程序:

    element.addEventListener(event, handler[, options]);
    
    1
    • event :事件名称。(例如 click
    • handler :处理程序。
    • options :附加可选对象:
      • once :如果为 true ,那么会在被触发后自动删除监听器。一次性事件处理
      • capture :事件处理的阶段
      • passive :如果为 true ,那么处理程序将不会调用 preventDefault() (浏览器默认行为)。
  • 对元素移除处理程序:

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

    移除的程序必须相同。所以存储在变量中。

  • 对元素添加多个处理程序:
    多次调用 addEventListener :

    <input id="elem" type="button" value="Click me"/>
    
    <script>
      function handler1() {
        alert('Thanks!');
      };
    
      function handler2() {
        alert('Thanks again!');
      }
    
      elem.onclick = () => alert("Hello");
      elem.addEventListener("click", handler1); // Thanks!
      elem.addEventListener("click", handler2); // Thanks again!
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    可以 同时 使用 DOM 属性和 addEventListener 来设置处理程序。通常我们只使用其中一种方式。

对于某些事件只能使用 addEventListener 添加处理程序:

🌰 例子 / DOMContentLoaded 事件,该事件在文档加载完成并且 DOM 构建完成时触发。:

document.addEventListener("DOMContentLoaded", function() {
  alert("DOM built");
});
1
2
3

# 事件对象

当事件发生时,浏览器会创建一个 event 对象,将详细信息放入其中,并将其作为参数传递给处理程序。

🌰 例子 / 从 event 对象获取鼠标指针的坐标:

<input type="button" value="Click me" id="elem">
<script>
  elem.onclick = function(event) {
    // 显示事件类型、元素和点击的坐标
    alert(event.type + " at " + event.currentTarget);
    alert("Coordinates: " + event.clientX + ":" + event.clientY);
  };
</script>
1
2
3
4
5
6
7
8

事件对象的一般属性:

  • event.type : 事件的类型。
  • event.currentTarget :处理事件的元素。与 this 相同,除非处理程序是一个箭头函数,或者它的 this 被绑定到了其他东西上,之后可以从 event.currentTarget 获取元素了。
  • event.clientX / event.clientY :指针事件的指针的窗口相对坐标。

# 对象处理程序

处理事件的程序除了可以分配函数,还可以将一个 对象 分配为事件处理程序。回调用对象中的 handleEvent 方法。

🌰 例子:

<button id="elem">Click me</button>

<script>
  let obj = {
    handleEvent(event) {
      alert(event.type + " at " + event.currentTarget);
    }
  };

  elem.addEventListener('click', obj);
</script>
1
2
3
4
5
6
7
8
9
10
11

🌰 例子 / 使用一个类:

<button id="elem">Click me</button>

<script>
  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);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

🌰 例子 / handleEvent 调用其他特定于事件的方法:

<button id="elem">Click me</button>

<script>
  class Menu {
    handleEvent(event) {
      // mousedown -> onMousedown
      let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
      this[method](event);
    }

    onMousedown() {
      elem.innerHTML = "Mouse button pressed";
    }

    onMouseup() {
      elem.innerHTML += "...and released.";
    }
  }

  let menu = new Menu();
  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

将事件处理程序明确地分离了出来,这样更容易进行代码编写和后续维护。

# 冒泡

🌰 例子 / 引入:

<div onclick="alert('The handler!')">
  <em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em>
</div>
1
2
3

事件处理程序分配给标签 <div> 。但是点击其中嵌套的标签( <em><code> ) 都会触发该事件处理程序。

冒泡的原理(Bubbling):当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。

🌰 例子:

::: demo[vanilla]

<style>
  #example * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>
<html>
  <form id="example" onclick="alert('form')"> FORM
    <div onclick="alert('div')">DIV
      <p onclick="alert('p')">P</p>
    </div>
  </form>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13

:::

点击内部的 <p> 会首先运行 onclick

  1. 在该 <p> 上的。
  2. 然后是外部 <div> 上的。
  3. 然后是外部 <form> 上的。
  4. 以此类推,直到最后的 document 对象。

这个过程就是冒泡。

** 几乎所有事件都会冒泡。** 例外: focus 事件不会冒泡。

# 目标元素 event.target

父元素上的处理程序始终可以获取事件实际发生位置的详细信息。引发事件的那个嵌套层级最深的元素被称为目标元素 ,通过 event.target 访问。

thisevent.target 的区别:

  • event.target :引发事件的目标元素。冒泡过程不会变化。
  • this当前元素。随着冒泡过程,是当前正在运行的处理程序的元素。

🌰 例子:

这个例子中,处理程序 form.onclick ,那么它可以捕获表单内的所有点击。所以无论点击发生在哪个元素,都会冒泡到这个处理程序,所以此时:

  • event.target :实际被点击的元素;
  • this<form> 元素,因为处理程序在它上面运行。

event.target 可以等于 this ,在点击发生在 <form> 时)

# 停止冒泡

用于停止冒泡的方法是 event.stopPropagation()

事件的冒泡一旦发生,通常会一直上升到 <html> 再到 document ,甚至到达 window ,回调用路径上所有的处理程序。

但是任意处理程序都可以决定事件已经被完全处理,并停止冒泡。

🌰 例子:

<body onclick="alert(`the bubbling doesn't reach here`)">
  <button onclick="event.stopPropagation()">Click me</button>
</body>
1
2
3

这里的按钮的 body.onclick 事件会停止工作。

event.stopImmediatePropagation() 可以用于 停止冒泡,并且阻止当前元素上的处理程序运行,(意味着,其他处理程序不会执行)。

注意

非必要,不阻止。阻止冒泡后可能会出现的问题:

  • 当创建了一个嵌套菜单,每个子菜单各自处理对自己的元素的点击事件,并调用 stopPropagation ,以便不会触发外部菜单;当在外部要捕获窗口的 用户的点击时,阻止冒泡后,这个区域不能被捕获到。

通常,没有真正的必要去阻止冒泡。如果真的要阻止冒泡,通常使用其他方法 如自定义事件;还可以数据写入一个处理程序中的 event 对象,并在另一个处理程序中读取该数据,这样就可以向父处理程序传递有关下层处理程序的信息。

# 捕获

DOM 描述事件传播的三个阶段:

  • 捕获阶段:事件(从 window)走进元素
  • 目标阶段:事件到达目标元素;
  • 冒泡阶段:事件从元素上开始冒泡。

捕获阶段一般很少用到,所以通常看不见。

为了在捕获阶段捕获事件,需要将处理程序的 capture 选项设置为 trueaddEventListener 中的选项参数):

  • false (默认值),则在冒泡阶段设置处理程序。
  • true ,则在捕获阶段设置处理程序。

虽然形式上有三个阶段,但实际上第二阶段没有被单独处理,捕获阶段和冒泡阶段的处理程序都在该阶段被触发。

🌰 例子:

<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>

<form>FORM
  <div>DIV
    <p>P</p>
  </div>
</form>

<script>
  for(let elem of document.querySelectorAll('*')) {
    elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
    elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

例子中,为文档中的 每个 元素都设置了点击处理程序,点击 <p> 时:

  • HTMLBODYFORMDIV (捕获阶段第一个监听器):
  • P (目标阶段,触发两次,因为设置了两个监听器:捕获和冒泡)
  • DIVFORMBODYHTML (冒泡阶段,第二个监听器)。

event.eventPhase 可以获得事件的阶段数。但它很少被使用,因为通常是从处理程序中了解到它。

提示

  • 要移除处理程序, removeEventListener 需要同一阶段。如果 addEventListener(..., true) ,那么应该在 removeEventListener(..., true) 中提到同一阶段,以正确删除处理程序。

  • 同一元素的同一阶段的监听器按其设置顺序运行。如果在同一阶段有多个事件处理程序,并通过 addEventListener 分配给了相同的元素,则它们的运行顺序与创建顺序相同:

    elem.addEventListener("click", e => alert(1)); // 会先被触发
    elem.addEventListener("click", e => alert(2));
    
    1
    2

# 事件委托

事件的捕获与冒泡阶段 允许了 事件的委托模式。

思路:如果有类似的方式处理的元素,那么就不用为每一个元素都分配一个处理程序,而是要将单个处理程序放在共同的祖先。

🌰 例子:

要实现如上的 在点击时高亮显示该单元格。无论有多少个单元格,都可以分配处理的程序。

可以在 table 元素上设置一个 捕获所有点击 的处理程序(利用事件的捕获阶段),这样不用担心表格有多少个单元格,也可以随时动态增加 / 移除 <td> 元素,点击高粱依然有效:

let selectedTd;
table.onclick = function(event) {
  let target = event.target
  if(target.tagName != 'TD') {
    return // 不处理没有点在td上的事件
  }
  highlisht(target)
}

function highlight(td) {
  if (selectedTd) { // 移除现有的高亮显示,如果有的话
    selectedTd.classList.remove('highlight');
  }
  selectedTd = td;
  selectedTd.classList.add('highlight'); // 高亮显示新的 td
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

但由于 <td> 内部还有许多嵌套的元素标签,所以如果点击例如 <strong> 元素时, strong 元素称为 event.target ,这样也要考虑在 td 的范围内,改进为:

table.onclick = function(event) {
  let td = event.target.closest('td'); // (1)

  if (!td) return; // (2)

  if (!table.contains(td)) return; // (3)

  highlight(td); // (4)
};
1
2
3
4
5
6
7
8
9

使用元素的 closet 属性,找到最近的祖先元素。

# 事件委托给 标记的行为

🌰 例子 / 如果要编写一个有 “保存”、“加载” 和 “搜索” 等按钮的菜单。并且,这里有一个具有 saveloadsearch 等方法的对象。

要匹配它们对应到的事件,如果每次单独分配处理程序过于麻烦。可以考虑给整个菜单添加一个处理程序,并且为具有方法调用的按钮添加 data-action 特性:

<button data-action="save">Click to Save</button>
1

完整代码:

注意: this.onclick.bind(this) 要绑定 Menu 对象的 this 上下文对象,否则在处理程序内部 引用的是 element 对象而不是 Menu 对象。

提示

使用事件委托的好处

  • 不需要编写代码给每一个按钮分配单独的处理程序。只需要创建一个处理程序获取该元素的标记,以分配对应的行为;
  • 并且可以随时灵活移除 / 增加按钮。

# 行为模式

事件委托将行为以 声明的方式 添加到具有特殊特性和类的元素中。

行为模式的两个部分:

  • 将自定义特性添加到描述其行为的元素。
  • 用文档范围级的处理程序追踪事件,如果事件发生在具有特定特性的元素上,则执行行为。

🌰 例子 / 计数器:

可以根据需要使用 data-counter 特性到相应的元素上。使用事件委托可以根据该行为特性处理事件。

提示

对于文档级的处理程序始终使用的是 addEventListener

将事件处理程序分配给 document 对象时,应该始终使用 addEventListener ,而不是 document.on<event> ,因为后者会引起冲突:新的处理程序会覆盖旧的处理程序

对于实际项目来说。在 document 上有许多由代码的不同部分设置的处理程序,这是很正常的。

🌰 例子 / 显示隐藏切换器 toggle:

因为添加了对 toggleId 的行为的事件处理程序。所以现在向元素添加功能,只需要添加一个行为模式的特性 data-toggle-id , 无需为每个这样的元素编写 JavaScript。只需要使用行为。文档级处理程序使其适用于页面的任意元素。

提示

事件委托的使用,通常用于为许多相似的元素添加相同的处理,但不仅限于此。

好处:

  • 简化初始化并节省内存:无需添加许多处理程序。
  • 更少的代码:添加或移除元素时,无需添加 / 移除处理程序。

局限:

  • 首先,事件必须冒泡。而有些事件不会冒泡。此外,低级别的处理程序不应该使用 event.stopPropagation()
  • 其次,委托可能会增加 CPU 负载,因为容器级别的处理程序会对容器中任意位置的事件做出反应,而不管我们是否对该事件感兴趣。但是,通常负载可以忽略不计,所以我们不考虑它。

# 浏览器事件默认行为

常见的默认行为:

  • 点击一个链接,触发导航到该 URL;
  • 点击表单的提交按钮,触发提交到服务器的行为;
  • 在文本上按下鼠标按钮并移动,选中文本;

# 阻止浏览器的默认行为

两种方法:

  • 使用 event 对象。有一个 event.preventDefault() 方法。
  • 使用 on<event> (而不是 addEventListener )分配的,那返回 false 也同样有效。

🌰 例子:

事件处理程序返回的值通常会被忽略。唯一的例外是从使用 on<event> 分配的处理程序中返回的 return false

在所有其他情况下, return 值都会被忽略。并且,返回 true 没有意义。

🌰 例子 / 处理导航菜单的默认行为:

<script>
  let menu = document.querySelector("#menu")
	menu.onclick = function(event) {
    if(event.target.nodeName != 'A') return;
    
    let href = event.target.getAttribute('href')
    alert(href);
    
    return false; 
  }
</script>
1
2
3
4
5
6
7
8
9
10
11

# passive

addEventListener 的可选项 passive: true 向浏览器发出信号,表明处理程序将不会调用 preventDefault()

移动设备上会发生一些事件,例如 touchmove (当用户在屏幕上移动手指时),默认情况下会导致滚动,但是可以使用处理程序的 preventDefault() 来阻止滚动。

因此,当浏览器检测到此类事件时,它必须首先处理所有处理程序,然后如果没有任何地方调用 preventDefault ,则页面可以继续滚动。但这可能会导致 UI 中不必要的延迟和 “抖动”。

passive: true 选项告诉浏览器,处理程序不会取消滚动。然后浏览器立即滚动页面以提供最大程度的流畅体验,并通过某种方式处理事件。

某些浏览器默认情况下, touchstarttouchmove 事件的 passivetrue

# 阻止事件冒泡

可以使用 event.defaultPrevented 来代替 event.stopPropagation() ,来通知其他事件处理程序,该事件已经被处理。

🌰 例子:

默认情况下,浏览器在 contextmenu 事件(单击鼠标右键)时,显示带有标准选项的上下文菜单。可以阻止它并显示我们自定义的菜单:

除了对于特定的元素,还可以实现文档范围内的:

<p>Right-click here for the document context menu</p>

<button id="elem">Right-click here for the button context menu</button>

  <script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Document context menu");
  };
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这个例子中,点击 elem 时,会得到两个菜单:按钮级和文档级(事件冒泡)的菜单。

解决方案,阻止事件冒泡:

<script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    event.stopPropagation();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Document context menu");
  };
</script>
1
2
3
4
5
6
7
8
9
10
11
12

但是阻止事件冒泡存在代价,现在 右键点击信息 相关的事件,都是被取消的。

采用另一个解决方案,检查是否已经被阻止了默认行为即可,如果已经阻止就不用处理:

<script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    if (event.defaultPrevented) return;

    event.preventDefault();
    alert("Document context menu");
  };
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

现在,如果有嵌套的元素,并且每个元素都有自己的上下文菜单,那么这也是可以运行的。只需确保检查每个 contextmenu 处理程序中的 event.defaultPrevented

# 自定义事件

自定义事件可用于创建图形组件。例如,基于 JavaScript 的菜单的根元素可能会触发 open (打开菜单), select (有一项被选中)等事件来告诉菜单发生了什么。另一个代码可能会监听事件,并观察菜单发生了什么。

不仅可以出于自身目的而创建的全新事件,还可以生成例如 clickmousedown 等内建事件。这可能会有助于自动化测试。

# 事件的构造器

let evnet = new Event(type[, options])
1
  • type :事件类型;

  • options :两个可选属性对象配置:

    • bubbles: true/false :如果为 true ,那么事件会冒泡。
    • cancelable: true/false :如果为 true ,那么默认行为就会被阻止。

    默认情况下,两个配置都为 false ,即事件不冒泡并且不阻止默认行为。

# 调用自定义事件

使用 elem.dispatchEvent(event) 调用在元素上运行 event 。然后,处理程序会对它做出反应,就好像它是一个常规的浏览器事件一样。

🌰 例子:

<button id="elem" onclick="alert('Click!');">Autoclick</button>

<script>
  let elem = document.querySelector("#elem")
  let event = new Event("click");
  elem.dispatchEvent(event);
</script>
1
2
3
4
5
6
7

这个例子中, click 事件是用 JavaScript 初始化创建的。处理程序工作方式和点击按钮的方式相同。

# 区分自定义事件

使用 event.isTrusted 区分真实用户事件和 通过脚本生成的自定义事件:

  • 对于来自真实用户操作的事件, event.isTrusted 属性为 true
  • 对于脚本生成的事件, event.isTrusted 属性为 false

# 自定义事件的冒泡

🌰 例子 / 创建一个自定义的冒泡事件,并在 document 上捕获:

<h1 id="elem">
  Hello from the script
</h1>

<script>
  // 在 document 捕获 hello 事件
	document.addEventListener("hello", function(event) {
    alert("Hello from" + event.target.tagName)
  })
  
  let event = new Event("hello", {bubbls: true});
  elem.dispatch(event)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 应该对自定义事件使用 addEventListener ,因为 on<event> 仅存在于内建事件中, document.onhello 则无法运行。
  • 必须设置 bubbles:true ,否则事件不会向上冒泡。

内建事件( click )和自定义事件( hello )的冒泡机制相同。自定义事件也有捕获阶段和冒泡阶段。

# 自定义事件的类型

通常的 UI 事件类有:

  • UIEvent
  • FocusEvent
  • MouseEvent
  • WheelEvent
  • KeyboardEvent

相关的 UI 事件规定 https://www.w3.org/TR/uievents

要创建这样类型的事件,使用它们而不是 new Event ,正确的构造器允许为该类型的事件指定标准属性。

🌰 例子:

let event = new MouseEvent("click", {
  bubbles: true,
  cancelable: true,
  clientX: 100,
  clientY: 100
});

alert(event.clientX);
1
2
3
4
5
6
7
8

对于通用的 Event 构造器,不允许这样:

let event = new Event("click", {
  bubbles: true, // 构造器 Event 中只有 bubbles 和 cancelable 可以工作
  cancelable: true,
  clientX: 100,
  clientY: 100
});

alert(event.clientX); // undefined,未知的属性被忽略了!
1
2
3
4
5
6
7
8

技术上,可以通过在创建后直接分配 event.clientX=100 来解决这个问题。所以,这是一个方便和遵守规则的问题。浏览器生成的事件始终具有正确的类型。

# 自定义事件的事件类型

要全新自定义事件类型,使用 new CustomEvent

从技术上讲,CustomEvent (opens new window)Event 一样。除了一点不同。在第二个参数(对象)中,可以为想要与事件一起传递的 任何自定义信息添加一个附加的属性 detail

事件类描述了它是「什么类型的事件」,如果事件是自定义的,那么应该使用 CustomEvent 来明确它是什么。

🌰 例子:

<h1 id="elem">Hello for John!</h1>

<script>
  // 事件附带给处理程序的其他详细信息
  elem.addEventListener("hello", function(event) {
    alert(event.detail.name);
  });

  elem.dispatchEvent(new CustomEvent("hello", {
    detail: { name: "John" }
  }));
</script>
1
2
3
4
5
6
7
8
9
10
11
12

detail 属性可以有任何数据。

  • 通常在创建后将任何属性分配给常规的 new Event 对象中。
  • 但是 CustomEvent 提供了特殊的 detail 字段,以避免与其他事件属性的冲突。

# 自定义事件的默认行为

对于新的自定义的事件,绝对没有默认的浏览器行为,但是分派此类事件的代码可能有自己的计划,触发该事件之后应该做什么。

🌰 例子 /

<pre id="rabbit">
  |\   /|
   \|_|/
   /. .\
  =\_Y_/=
   {>o<}
</pre>
<button onclick="hide()">Hide()</button>
1
2
3
4
5
6
7
8
<script>
	function hide() {
    let event = new CustomEvent("hide", {
      cancelable: true // 没有这个标志,preventDefault 将不起作用
    });
    if (!rabbit.dispatchEvent(event)) {
      alert('The action was prevented by a handler');
    } else {
      rabbit.hidden = true;
    }
  }

  rabbit.addEventListener('hide', function(event) {
    if (confirm("Call preventDefault?")) {
      event.preventDefault();
    }
  });
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 事件的同步事件

通常事件是在队列中处理的。也就是说:如果浏览器正在处理 onclick ,这时发生了一个新的事件,例如鼠标移动了,那么它的处理程序会被排入队列,相应的 mousemove 处理程序将在 onclick 事件处理完成后被调用。

例外:当一个事件是在另一个事件中发起的。例如使用 dispatchEvent 。这类事件将会被立即处理,即在新的事件处理程序被调用之后,恢复到当前的事件处理程序。

🌰 例子 / menu-open 事件是在 onclick 事件执行过程中被调用的。它会被立即执行,而不必等待 onclick 处理程序结束:

<button id="menu">Menu (click me)</button>
<script>
  menu.onclick = function() {
    alert(1);

    menu.dispatchEvent(new CustomEvent("menu-open", {
      bubbles: true
    }));

    alert(2);
  };

  // 在 1 和 2 之间触发
  document.addEventListener('menu-open', () => alert('nested'));
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

嵌套事件 menu-open 会在 document 上被捕获。嵌套事件的传播和处理先被完成,然后处理过程才会返回到外部代码( onclick )。

这不只是与 dispatchEvent 有关,还有其他情况。如果一个事件处理程序调用了触发其他事件的方法 —— 它们同样也会被以嵌套的方式同步处理。

如果想让 onclick 不受 menu-open 或者其它嵌套事件的影响,优先被处理完毕。那么可以将 dispatchEvent (或另一个触发事件的调用)放在 onclick 末尾,或者最好将其包装到零延迟的 setTimeout 中:

<script>
  menu.onclick = function() {
    alert(1);

    setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
      bubbles: true
    })));

    alert(2);
  };

  document.addEventListener('menu-open', () => alert('nested'));
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

现在, dispatchEvent 在当前代码执行完成之后异步运行,包括 menu.onclick ,因此,事件处理程序是完全独立的。

📢 上次更新: 2022/09/02, 10:18:16