事件!事件!事件!


参考链接

最近在重学基础,看到事件,做一个总结。
路线 : 事件注册 -> 事件监听 -> 事件处理

概述

事件和事件处理提供了javascript中的用于反应用户web页面打开的时间中发生的事件的核心技术,包括页面正在准备展示时发生的事件、由用户与页面交互产生的事件、由于媒体流播放或动画定时等许多其他原因引起的事件。

最初,浏览器会获得一个页面的所有部分来解析,处理,绘制和呈现该页面给用户,然后浏览器将保持不变,直到请求和提取新页面为止。随之变为事件驱动的、基于reflow的页面渲染,浏览器将在处理,绘制,呈现内容之间重复循环,并等待一些新的事件触发器再次启动循环。事件触发器包括完成网络上的资源的加载(例如图像已被下载并且现在可以在屏幕上绘制),完成浏览器解析资源(例如HTML页面已被处理),用户与页面的内容的交互(例如,已经点击了按钮)。Douglas Crockford在几次讲座中有效地解释了这一变化,特别是他的话题 《An Inconvenient API: The Theory of the DOM》 (不方便的API:DOM理论),其中描述了从 original flow 到事件驱动浏览器的 flow 变化。(the change in flow from the original flow to the event driven browser.)
古老的原型和基于事件、flow、渲染的现代模型
后一种方法将最后一步从单一流程转变为永久循环,其中绘制paint之后是等待和处理新事件的发生。后一种方法的创新允许页面部分呈现,即使尚未获得资源;该方法也允许javascript影响事件驱动的行为。目前,Javascript代码的所有执行环境都使用事件和事件处理。

事件设计模式

事件系统(the event system)的核心是一个简单的编程设计模式。这种模式从一类事件的协议开始:

  • 用于描述事件名称的 string
  • 用于表示该事件的关键属性的数据结构的类型
  • 将发射(emit)该事件的Javascript对象。
    要使用该模式,需要:
  • 定义一个function,其作为一个议定的数据结构的参数
  • 通过使用前面的名称string与一个emit该事件的对象注册该function。

这些function被称为“listener(监听函数)”或者“handler(处理函数)”,如在自定义事件的文章中所述,可以使用完全自定义的代码轻松实现此模式。

Notable events

浏览器使用addEventListen(...)用作事件注册函数,该方法使用一个描述事件名称的string和一个事件处理函数作为参数。浏览器将大量对象定义为事件发射器,并定义了由对象生成的各种事件类型。

这里有一个持续维护的现代浏览器使用的标准事件的列表(目前还不完整)

一般来说,我们可以根据发出事件的对象来区分不同类型的事件,包括:

  • window 对象,列如浏览器窗口大小变化
  • window.screen 对象,如由于设备方向的变化
  • document 对象,包括加载,修改,用户交互和卸载页面
  • DOM(文档对象模型)树中的对象,包括用户交互或修改
  • 用于网络请求的XMLHttpRequest对象,媒体对象如音视频,当媒体流播放器改变状态。

一些关键事件
当页面完成渲染时,全局对象window会发出一个名为“load”的事件,这意味着所有资源已被下载和执行,从而运行脚本并显示图像。
当用户改变浏览器窗口大小的时候,window会发出一个名为“resize”的事件。
表示HTML文档的DOM对象 document 在文档加载完成时会发出一个名为“DOMContentLoaded”的事件。

event 对象的层次结构

下面是一幅
部分图


事件注册

对于浏览器提供的事件,上面有一个列表,这些事件可以直接使用:

1
2
3
// 比如说点击事件 (click)
const targetElement = document.querySelect('#targetELementID')
targetELement.addEventListen('click', () => console.log('click event happened'))

addEventListen(…)

addEventListen 是注册事件监听函数的广义方法,在W3C文档中记以 event handler IDL Atrribute,也能处理特定的on<event name>事件。
优点包括:

  • 允许注册多个事件
  • 当存在其他的库时,使用 DHTML 库或者 Mozilla extensions 不会出现问题。
  • 它提供了一种更精细的手段控制 listener 的触发阶段。(即可以选择捕获或者冒泡)。
  • 它对任何 DOM 元素都是有效的,而不仅仅只对 HTML 元素有效。

需要注意也有几项
当在事件分派的时候注册事件 并不会立刻触发事件,但是可能会在其他阶段触发,比如在捕获阶段注册的事件可能会在冒泡阶段被触发。
多个相同的事件处理器 如果是在同一个 eventTarget 上注册的话重复的实例会被舍弃,所以这么做不会使得 EventListener 被调用两次,也不需要用 removeEventListener 手动清除多余的EventListener ,因为重复的都被自动抛弃了。(应该是FIFO)
this的值 通常来说是触发元素的引用,当使用 addEventListener() 为一个元素注册事件的时候,它总是引用事件处理程序附加到的元素,而不是event.target,其与传递给句柄的 event 参数的 currentTarget 属性的值一样。但是当 直接在HTML中使用內联的方式注册事件的情况下 this 将会指向 window(严格模式下为 undefined)

语法:

1
2
3
> target.addEventListener(type, listener[, options]);
> target.addEventListener(type, listener[, useCapture]);
>

type
表示监听事件类型的字符串。(个人认为可以当作用于描述事件的名称字符串)
listener
当所监听的事件类型触发时,会接收到一个事件通知(实现了 Event 接口的对象)对象。listener 必须是一个实现了 EventListener 接口的对象,或者是一个函数(基本上所有的函数都实现了EventListener对象)
options
一个指定有关 listener 属性的可选参数对象。可用的选项如下:

  • capture: Boolean,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。(是否捕获)
  • once: Boolean,表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除。
  • passive: Boolean,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。
    useCapture
    Boolean,是指在DOM树中,注册了该listener的元素,是否会先于它下方的任何事件目标,接收到该事件。沿着DOM树向上冒泡的事件不会触发被指定为use capture(也就是设为true)的listener。当一个元素嵌套了另一个元素,两个元素都对同一个事件注册了一个处理函数时,所发生的事件冒泡和事件捕获是两种不同的事件传播方式。事件传播模式决定了元素以哪个顺序接收事件。即在捕获模式和冒泡模式中选择一个。进一步的解释可以查看 事件流JavaScript Event order 文档。 如果没有指定, useCapture 默认为 false 。

注册on-event处理程序

直接在HTML的on-event属性中写事件处理函数在W3C文档中被称为 event handler content attribute

我们可以以不同的方式为给定对象指定特定事件(例如单击)的on <…>事件处理程序:

  • 在元素上使用{eventtype}命名的HTML属性,例如: <button onclick =“return handleClick(event);”>
  • 或者通过从JavaScript设置相应的属性,例如: document.getElementById("mybutton").onclick = function(event){...}

请注意,每个对象对于给定的事件只能有一个事件处理程序(尽管该处理程序可以调用多个子处理程序)。
这就是为什么addEventListener()通常是更好的来获取事件通知的方式,特别是当期望相互独立地应用各种事件处理程序时,这即使是对于相同的事件 and/or 同一个元素也是如此。

还要注意,on-event handlers 是自动调用的,而不随程序员的意志(尽管可以 mybutton.onclick(myevent);),因为它们可以作为一个可以分配一个真正的处理函数的占位符。

对于 非元素对象 ,我们也可以使用生成事件的许多非元素对象(包括窗口,文档,XMLHttpRequest等)的属性来设置事件处理程序,例如:

1
xhr.onprogress = function() { ... }

由于历史原因,<body><frameset>元素上的某些属性/属性实际上在其父窗口对象上设置了事件处理程序。 (HTML规范名称为:onblur,onerror,onfocus,onload,onscroll。)

当事件处理程序被指定为HTML属性时,指定的代码将被包装到具有以下参数的函数中:

  • event == 对于所有事件处理程序,除了 onerror。
  • event, source, lineno, colno, onerror 函数的 error。请注意,事件参数实际上包含错误消息作为字符串。

对于this的指向,这篇文章有很详细的介绍

来自处理程序的返回值确定事件是否被取消。返回值的具体处理取决于事件的种类,详细信息请参见HTML规范中的“事件处理程序处理算法”

Mutation event

突变事件用于监测DOM元素是否发生改变
参考这里

构造函数

MutationObObserver()
参数

  • callback
    该回调函数会在指定的DOM节点(目标节点)发生变化时被调用.在调用时,观察者对象会传给该函数两个参数,第一个参数是个包含了若干个MutationRecord对象的数组,第二个参数则是这个观察者对象本身.

实例方法

1
2
3
void observe( Node target, optional MutationObserverInit options );
void disconnect();
Array takeRecords();

observe()
给当前观察者对象注册需要观察的目标节点,在目标节点(还可以同时观察其后代节点)发生DOM变化时收到通知.

1
2
3
4
void observe(
Node target,
optional MutationObserverInit options
);

参数

  • target
    观察该节点是否会发生DOM变化.
  • options
    一个MutationObserverInit对象,指定要观察的DOM变化类型.

注:向一个元素添加 observer 和 addEventListener 类似,注册多次不会有任何影响。即是说,如果你注册了两次,回调函数不会被调用两次,你也不必执行两次 disconnect() 以停止观察。换句话说,一旦某个元素被注册观察后,使用相同的 observer 实例再次注册不会发生任何变化。当然,如果回调对象不同,那么他会向这个元素添加另一个观察者。

disconnect()
让该观察者对象停止观察指定目标的DOM变化.直到再次调用其observe()方法,该观察者对象包含的回调函数都不会再被调用.

takeRecords()
清空观察者对象的记录队列,并返回里面的内容.

返回值

  • 返回一个包含了MutationRecords对象的数组.

MutationObserverInit

MutationObserverInit是一个用来配置观察者对象行为的对象,该对象可以拥有下面这些属性:

  • childList
    如果需要观察目标节点的子节点(新增了某个子节点,或者移除了某个子节点),则设置为true.

  • attributes
    如果需要观察目标节点的属性节点(新增或删除了某个属性,以及某个属性的属性值发生了变化),则设置为true.

  • characterData
    如果目标节点为characterData节点(一种抽象接口,具体可以为文本节点,注释节点,以及处理指令节点)时,也要观察该节点的文本内容是否发生变化,则设置为true.

  • subtree
    除了目标节点,如果还需要观察目标节点的所有后代节点(观察目标节点所包含的整棵DOM树上的上述三种节点变化),则设置为true.

  • attributeOldValue
    在attributes属性已经设为true的前提下,如果需要将发生变化的属性节点之前的属性值记录下来(记录到下面MutationRecord对象的oldValue属性中),则设置为true.

  • characterDataOldValue
    在characterData属性已经设为true的前提下,如果需要将发生变化的characterData节点之前的文本内容记录下来(记录到下面MutationRecord对象的oldValue属性中),则设置为true.

  • attributeFilter
    一个属性名数组(不需要指定命名空间),只有该数组中包含的属性名发生变化时才会被观察到,其他名称的属性发生变化后会被忽略.

MutationRecord

MutationRecord对象会作为第一个参数传递给观察者对象包含的回调函数,该对象有下面这些属性:

  • type String 如果是属性发生变化,则返回attributes.如果是一个CharacterData节点发生变化,则返回characterData,如果是目标节点的某个子节点发生了变化,则返回childList.
  • target Node 返回此次变化影响到的节点,具体返回那种节点类型是根据type值的不同而不同的. 如果type为attributes,则返回发生变化的属性节点所在的元素节点,如果type值为characterData,则返回发生变化的这个characterData节点.如果type为childList,则返回发生变化的子节点的父节点.
  • addedNodes NodeList 返回被添加的节点,或者为null.
  • removedNodes NodeList 返回被删除的节点,或者为null.
  • previousSibling Node 返回被添加或被删除的节点的前一个兄弟节点,或者为null.
  • nextSibling Node 返回被添加或被删除的节点的后一个兄弟节点,或者为null.
  • attributeName String 返回变更属性的本地名称,或者为null.
  • attributeNamespace String 返回变更属性的命名空间,或者为null.
  • oldValue String
    根据type值的不同,返回的值也会不同.如果type为 attributes,则返回该属性变化之前的属性值.如果type为characterData,则返回该节点变化之前的文本数据.如果type为childList,则返回null.

触发和创建事件

我们可以自己创建和分派DOM事件。这些事件通常称为合成事件,而不是浏览器本身触发的事件。

创建自定义事件

Events 可以使用 Event构造函数 创建如下:

1
2
3
4
5
6
7
var event = new Event('build');
// Listen for the event.
elem.addEventListener('build', function (e) { ... }, false);
// Dispatch the event.
elem.dispatchEvent(event);

绝大多数现代浏览器中都会支持这个构造函数(Internet Explorer 例外)。 要了解更为复杂的方法,可参考下面的 过时的方法 一节。

添加自定义数据 – CustomEvent()

要向事件对象添加更多数据,可以使用CustomEvent接口,并且detail属性可用于传递自定义数据。
例如,event 可以创建如下:

1
var event = new CustomEvent('build', { 'detail': elem.dataset.time });

下面的代码允许你在事件监听器中访问更多的数据:

1
2
3
function eventHandler(e) {
log('The time is: ' + e.detail);
}

触发内置事件

下面的例子演示了一个在复选框上点击(click)的模拟(就是说在程序里生成一个click事件),这个模拟点击使用了DOM方法. 参见这个动态示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function simulateClick() {
var event = new MouseEvent('click', {
'view': window,
'bubbles': true,
'cancelable': true
});
var cb = document.getElementById('checkbox');
var cancelled = !cb.dispatchEvent(event);
if (cancelled) {
// A handler called preventDefault.
alert("cancelled");
} else {
// None of the handlers called preventDefault.
alert("not cancelled");
}
}

取消事件和阻止传播

某些事件被指定为可取消。 对于这些事件,DOM实现通常具有与事件相关联的默认操作。 列如Web浏览器中的超链接。 当用户点击超链接时,默认动作通常是激活该超链接。 在处理这些事件之前,实现必须检查注册的事件监听器以接收事件并将事件分派给这些监听器。 这些 EventListener 然后可以选择取消实现的默认动作,或允许默认动作继续。 在浏览器中的超链接的情况下,取消操作将导致不激活超链接。

通过调用 EventpreventDefault 方法来完成取消。如果有一个或多个 EventListener 在事件的任何阶段调用了 preventDefault ,那么默认动作将会取消。

Event.preventDefault()

被包含在Event事件对象中,可以在event handler函数中使用,用于阻止事件的默认操作。

下面的例子显示了用preventDefault()阻止checkbox的开关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<head>
<title>preventDefault example</title>
</head>
<body>
<p>Please click on the checkbox control.</p>
<form>
<label for="id-checkbox">Checkbox</label>
<input type="checkbox" id="id-checkbox"/>
</form>
<script>
document.querySelector("#id-checkbox").addEventListener("click", function(event){
alert("preventDefault will stop you from checking this checkbox!")
event.preventDefault();
}, false);
</script>
</body>
</html>

取消冒泡

对冒泡的事件(存在非冒泡的事件),事件被调度到其目标 EventTarget ,并且发现的任何事件监听器都被触发。然后,冒泡事件将触发任何追加 EventTarget 的父链的附加事件侦听器,检查在每个连续的 EventTarget 上注册的任何事件侦听器。这种向上传播将继续并且包括 document 。注册为捕获者的EventListeners在此阶段不会被触发。 EventTargets 链从事件目标到树的顶部在事件的初始调度之前确定。如果在事件处理过程中发生树的修改,则事件流将基于树的初始状态进行。

与事件捕获相同,一般用于设置默认事件或者全局捕获事件,要关闭冒泡可以使用 event.stopPropagation() 这样可以阻止事件在冒泡阶段的所有传播。对于完整的跨浏览器体验可以使用:

1
2
3
4
5
function doSomething(e) {
if (!e) var e = window.event;
e.cancelBubble = true;
if (e.stopPropagation) e.stopPropagation();
}

使用方法如同 event.preventDefault()

事件循环模型 Event-loop

原文链接

JavaScript 的并发模型基于 “事件循环”。这个模型与像 C 或者 Java 这种其它语言中的模型着实不同。

####### 运行时概念

函数调用会形成一个栈帧

1
2
3
4
5
6
7
8
9
10
11
function foo(b) {
var a = 10;
return a + b + 11;
}
function bar(x) {
var y = 3;
return foo(x * y);
}
console.log(bar(7));

当调用 bar 时,创建了第一个帧 ,帧中包含了 bar 的参数和局部变量。当 bar 调用 foo 时,第二个帧就被创建,并被压到第一个帧之上,帧中包含了 foo 的参数和局部变量。当 foo 返回时,最上层的帧就被弹出栈(剩下 bar 函数的调用帧 )。当 bar 返回的时候,栈就空了。

对象被分配在一个堆中,一个用以表示一个内存中大的未被组织的区域。

队列

一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都与一个函数相关联。当栈为空时,从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因而创建了一个初始堆栈帧)。当栈再次为空的时候,也就意味着消息处理结束。

事件循环

之所以称为 事件循环,是因为它经常被用于类似如下的方式来实现:

1
2
3
while (queue.waitForMessage()) {
queue.processNextMessage();
}

如果当前没有任何消息,queue.waitForMessage 会同步等待消息到来。

“执行至完成”

每一个消息执行完成后,其它消息才会被执行。当你分析你的程序时,这点提供了一些优秀的特性,包括当一个函数运行时,它不能被取代且会在其它代码运行前先完成(而且能够修改这个函数控制的数据)。这点与C语言不同。例如,C语言中当一个程序在一个线程中运行时,它可以在任何点停止且可以在其它线程中运行其它代码。

这个模型的一个缺点在于当一个消息的完成耗时过长,网络应用无法处理用户的交互如点击或者滚动。浏览器用“程序需要过长时间运行”的对话框来缓解这个问题。一个比较好的解决方案是使消息处理变短且如果可能的话,将一个消息拆分成几个消息。

添加消息

在浏览器里,当一个事件出现且有一个事件监听器被绑定时,消息会被随时添加。如果没有事件监听器,事件会丢失。所以点击一个附带点击事件处理函数的元素会添加一个消息。其它事件亦然。

调用 setTimeout 函数会在一个时间段过去后在队列中添加一个消息。这个时间段作为函数的第二个参数被传入。如果队列中没有其它消息,消息会被马上处理。但是,如果有其它消息,setTimeout 消息必须等待其它消息处理完。因此第二个参数仅仅表示最少的时间 而非确切的时间。

零延迟

零延迟 (Zero delay) 并不是意味着回调会立即执行。在零延迟调用 setTimeout 时,其并不是过了给定的时间间隔后就马上执行回调函数。其等待的时间基于队列里正在等待的消息数量。在下面的例子中,”this is just a message” 将会在回调 (callback) 获得处理之前输出到控制台,这是因为延迟是要求运行时 (runtime) 处理请求所需的最小时间,但不是有所保证的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function () {
console.log('this is the start');
setTimeout(function cb() {
console.log('this is a msg from call back');
});
console.log('this is just a message');
setTimeout(function cb1() {
console.log('this is a msg from call back1');
}, 0);
console.log('this is the end');
})();
// "this is the start"
// "this is just a message"
// "this is the end"
// "this is a msg from call back"
// "this is a msg from call back1"

多个运行时互相通信

一个 web worker 或者一个跨域的 iframe 都有它们自己的栈,堆和消息队列。两个不同的运行时只有通过 postMessage 方法进行通信。这个方法会给另一个运行时添加一个消息如果后者监听了 message 事件。

绝不阻塞

一个很有趣的事件循环 (event loop) 模型特性在于,Javascript 跟许多其它语言不同,它永不阻塞。通常由事件或者回调函数进行 I/O (input/output)处理 。所以当一个应用正等待 IndexedDB 的查询的返回或者一个 XHR 的请求返回时,它仍然可以处理其它事情例如用户输入。

例外是存在的,如 alert 或者同步 XHR,但应该尽量避免使用它们。注意,例外的例外也是存在的(但通常是实现错误而非其它原因)。