面试准备-事件(Event)

452 阅读16分钟

简介

首先浏览器的事件机制跟Node.js事件机制的概念是不同的,我们今天复习的过程中分别讲述一下两边的情况,以及一些相关使用场景。

浏览器事件机制

首先浏览器事件不断的迭代出来的,所以有很多不同的版本以及标准,针对光针对DOM的标准就有至少有三种(不过不知道现在是否还按照这种方式进行分类),那么分别是:DOM0, DOM2, DOM3

DOM0 事件

官方文档中暂时没有找到相关内容,不过我在wiki找到了一些内容。主要是关于介绍DOM0的两种事件模式

  • 内联模式: 以下是W3C在叙述按钮时的一个案例
<button 
    type=button
    onclick="alert('This 15-20 minute piece was composed by George Gershwin.')">
    Show hint
</button>

其中我们可以看到按钮button标签注册onclick点击事件,这就是DOM0事件触发机制的定义,即在html的文档片段上,直接书写在javascript脚本。 下面是全部的html文档定义的所有元素都存在的事件类型(当然并不能确保所有的都是DOM0时代产生的)

  • onauxclick
  • onbeforematch
  • onblur*
  • oncancel
  • oncanplay
  • oncanplaythrough
  • onchange
  • onclick
  • onclose
  • oncontextlost
  • oncontextmenu
  • oncontextrestored
  • oncopy
  • oncuechange
  • oncut
  • ondblclick
  • ondrag
  • ondragend
  • ondragenter
  • ondragleave
  • ondragover
  • ondragstart
  • ondrop
  • ondurationchange
  • onemptied
  • onended
  • onerror*
  • onfocus*
  • onformdata
  • oninput
  • oninvalid
  • onkeydown
  • onkeypress
  • onkeyup
  • onload*
  • onloadeddata
  • onloadedmetadata
  • onloadstart
  • onmousedown
  • onmouseenter
  • onmouseleave
  • onmousemove
  • onmouseout
  • onmouseover
  • onmouseup
  • onpaste
  • onpause
  • onplay
  • onplaying
  • onprogress
  • onratechange
  • onreset
  • onresize*
  • onscroll*
  • onsecuritypolicyviolation
  • onseeked
  • onseeking
  • onselect
  • onslotchange
  • onstalled
  • onsubmit
  • onsuspend
  • ontimeupdate
  • ontoggle
  • onvolumechange
  • onwaiting
  • onwheel

具体功能大家可以自行根据官方文档进行学习

  • 传统模式:
<html>
    <body>
        <button 
            id="btn"
            type=button>
            Show hint
        </button>
        <script>
            document.getElementById('btn').onclick = function () {
                alert('This 15-20 minute piece was composed by George Gershwin.')
            }
        </script>
    </body>
</html>

那么无论是传统模式还是内联模式 都只允许注册一个监听事件,如果同时使用两个模式,基本传统模都会覆盖内联模式

DOM2 事件

那么根据官方文档的定义:

DOM Level 2 事件模型的设计有两个主要目标。第一个目标是设计一个通用事件系统,它允许注册事件处理程序,通过树形结构描述事件流,并为每个事件提供基本的上下文信息。此外,该规范将为用户界面控制和文档突变通知提供标准事件模块,包括为每个事件模块定义的上下文信息。

事件模型的第二个目标是提供当前在 DOM Level 0(也就是DOM0) 浏览器中使用的事件系统的一个公共子集。这旨在促进现有脚本和内容的互操作性。预计这一目标不会完全向后兼容。但是,规范会在可能的情况下尝试实现这一点。

原文在中间的位置还讲解关于以下几个内容的大致介绍:

  1. 事件流
  2. 事件本身与EventTarget以及EventListener的关系
  3. 事件捕获跟EventTarget以及EventListener的关系
  4. 事件冒泡跟EventTarget以及EventListener的关系

我这边暂时先掠过这些内容,先给大家复习DOM2的事件注册情况

EventTarget

接口 EventTarget(在 DOM Level 2 中引入)

EventTarget 接口由支持 DOM 事件模型的实现中的所有节点实现。因此,可以通过在 Node 接口的实例上使用特定于绑定的转换方法来获得此接口。该接口允许在 EventTarget 上注册和删除 EventListener,并将事件分派到该 EventTarget

那么根据介绍DOM对象给我们提供了EventTarget的事件接口,当然在全局环境下,也有它对应的一个构造函数 EventTarget

注意: 在ES官方定义中是找不到EventTarget构造函数对象的,EventTarget是作为宿主对象中的函数对象提供在全局上下文的

那么下方有关于EventTarget接口所定义的属性方法

// Introduced in DOM Level 2:
interface EventTarget {
  void               addEventListener(in DOMString type, 
                                      in EventListener listener, 
                                      in boolean useCapture);
  void               removeEventListener(in DOMString type, 
                                         in EventListener listener, 
                                         in boolean useCapture);
  boolean            dispatchEvent(in Event evt)
                                        raises(EventException);
};

我们分别来看一下针对这三个方法的介绍是啥

addEventListener

此方法允许在事件目标上注册事件侦听器。如果在处理事件时将 EventListener 添加到 EventTarget,则不会被当前操作触发,但可能会在事件流的后期阶段(例如冒泡阶段)触发。

如果在同一个 EventTarget 上使用相同的参数注册了多个相同的 EventListener,则丢弃重复的实例。它们不会导致 EventListener 被调用两次,并且由于它们被丢弃,

因此不需要使用 removeEventListener 方法删除它们。

Parameters(方法参数)

  • type of type DOMString 用户正在注册的事件类型

  • listener of type EventListener listener 参数采用用户实现的接口,其中包含事件发生时要调用的方法。

  • useCapture of type boolean

如果为true,useCapture 表示用户希望启动捕获。启动捕获后,指定类型的所有事件将被分派到已注册的 EventListener,然后再分派到树中它们下方的任何 EventTargets。通过树向上冒泡的事件不会触发指定使用捕获的 EventListener

removeEventListener

此方法允许从事件目标中删除事件侦听器。如果在处理事件时将 EventListener 从 EventTarget 中删除,则不会被当前操作触发。 EventListener 在被移除后永远不能被调用。

使用未标识 EventTarget 上任何当前注册的 EventListener 的参数调用 removeEventListener 无效。

Parameters(方法参数)

  • type of type DOMString 指定要移除的 EventListener 的事件类型

  • listener of type EventListener EventListener 参数指示要删除的 EventListener。

  • useCapture of type boolean 指定要移除的 EventListener 是否注册为捕获侦听器。如果一个监听器被注册了两次,一个有捕获,一个没有,每个都必须单独删除。删除捕获侦听器不会影响同一侦听器的非捕获版本,反之亦然。

dispatchEvent

此方法允许将事件分派到实现事件模型中。以这种方式分派的事件将具有与直接由实现分派的事件相同的捕获和冒泡行为。事件的目标是调用 dispatchEvent 的 EventTarget。

Parameters(方法参数)

  • evt of type Event 指定用于处理事件的事件类型、行为和上下文信息。

Return Value(返回值)

boolean

dispatchEvent 的返回值指示是否有任何侦听器处理了称为 preventDefault 的事件。如果调用 preventDefault,则值为 false,否则值为 true

案例

const domEl = document.querySelector(*)
// 注册事件
domEl.addEventListener('click', () =>{
    // doSomeThing
    ...
    console.log('i am click event')
})

const noPreventDefault = domEl.dispatchEvent(new Event(
    "click",
    {
        "bubbles":true,
        "cancelable":false
    }
))
// output: 'i am click event'

// true
console.log(noPreventDefault)

EventListener

EventListener 接口是处理事件的主要方法。用户实现 EventListener 接口并使用 AddEventListener 方法在 EventTarget 上注册他们的监听器。用户在完成使用侦听器后,还应从其 EventTarget 中删除其 EventListener。

当使用 cloneNode 方法复制节点时,附加到源节点的事件侦听器不会附加到复制的节点。如果用户希望将相同的 EventListener 添加到新创建的副本中,则用户必须手动添加它们。

// Introduced in DOM Level 2:
interface EventListener {
  void    handleEvent(in Event evt);
};
handleEvent

每当发生为其注册了 EventListener 接口的类型的事件时,都会调用此方法。

Parameters(方法参数)

  • evt of type Event 事件包含有关事件的上下文信息。它还包含用于确定事件流和默认操作的 stopPropagation 和 preventDefault 方法。

Event

Interface Event(在 DOM Level 2 中引入)

Event 接口用于向处理事件的处理程序提供有关事件的上下文信息。实现 Event 接口的对象通常作为第一个参数传递给事件处理程序。通过从 Event 派生附加接口,将更具体的上下文信息传递给事件处理程序,这些接口包含与它们所伴随的事件类型直接相关的信息。这些派生接口也由传递给事件监听器的对象实现。

接口定义

interface Event {

  // PhaseType
  const unsigned short      CAPTURING_PHASE                = 1;
  const unsigned short      AT_TARGET                      = 2;
  const unsigned short      BUBBLING_PHASE                 = 3;

  readonly attribute DOMString        type;
  readonly attribute EventTarget      target;
  readonly attribute EventTarget      currentTarget;
  readonly attribute unsigned short   eventPhase;
  readonly attribute boolean          bubbles;
  readonly attribute boolean          cancelable;
  readonly attribute DOMTimeStamp     timeStamp;
  void               stopPropagation();
  void               preventDefault();
  void               initEvent(in DOMString eventTypeArg, 
                               in boolean canBubbleArg, 
                               in boolean cancelableArg)[方法已废弃];
};

常量组 PhaseType

用于定义指示正在处理事件流的哪个阶段。

定义的常量包括以下

  • AT_TARGET

当前正在目标 EventTarget 处评估该事件。

  • BUBBLING_PHASE

当前的事件阶段是冒泡阶段。

  • CAPTURING_PHASE

当前的事件阶段是捕获阶段。

属性

  • bubbles of type boolean, readonly

    用于指示事件是否为冒泡事件。如果事件可以冒泡,则值为 true,否则值为 false。

  • cancelable of type boolean, readonly

    用于指示事件是否可以阻止其默认操作。如果可以阻止默认操作,则值为 true,否则值为 false。

  • currentTarget of type EventTarget, readonly

    用于指示当前正在处理其 EventListeners 的 EventTarget。这在捕获和冒泡期间特别有用。

  • eventPhase of type unsigned short, readonly

    用于指示当前正在评估事件流的哪个阶段。

  • target of type EventTarget, readonly

    用于指示最初将事件分派到的 EventTarget。

  • timeStamp of type DOMTimeStamp, readonly

    用于指定创建事件的时间(相对于纪元的毫秒数)。由于某些系统可能不提供此信息,timeStamp 的值可能不适用于所有事件。当不可用时,将返回值 0。纪元时间的示例是系统启动时间或 1970 年 1 月 1 日 0:0:0 UTC。

  • type of type DOMString, readonly

    事件的名称(不区分大小写)。该名称必须是 XML 名称

方法

initEvent(已废弃)

因为是废弃方法我这里就不介绍,感兴趣的小伙伴可以自己点击原文查看

preventDefault

如果一个事件是可取消的,则 preventDefault 方法用于表示该事件将被取消,这意味着任何通常由实现作为事件结果而采取的默认操作都不会发生。如果在事件流的任何阶段调用 preventDefault 方法,则事件被取消。与该事件关联的任何默认操作都不会发生。对不可取消的事件调用此方法无效。一旦调用了 preventDefault ,它将在事件传播的其余部分保持有效。此方法可用于事件流的任何阶段。

stopPropagation

stopPropagation 方法用于在事件流期间防止事件的进一步传播。如果任何 EventListener 调用此方法,则事件将停止在树中传播。在事件流停止之前,该事件将完成对当前 EventTarget 上的所有侦听器的分派。此方法可用于事件流的任何阶段。

createEvent接口(已废弃)

同上所述,感兴趣的小伙伴自己看文档

事件类别

除上述的API跟接口的定义以外,DOM2的标准规范还定义几个关于标准事件的类型,他们分别是

  • User Interface event types

  • Mouse event types

  • Mutation event types

  • HTML event types

其中详细事件类型以及介绍我就不赘述了,大家可以自己看文档

总结

我们发现从DOM2开始就已经出现了事件监听器的概念,包括规范了整体的事件传播流,定义了基础的接口Event, EventTarget, EventListener。 对于我们的好处在基于相同的事件我们可以挂载多个监听方法,以及控制整体的事件流走向。

当然这里肯定会有人问,为什么没有DOM1。作者去找了一下,我个人的感觉是,并不是没有DOM1标准,而是DOM1标准中。没有对事件是没有进行改善的。如果大家发现什么点是作者忽略的,欢迎留言提醒。原文地址

DOM2 事件 -> 最新版本事件

根据作者对比,关于DOM3的一些原文叙述,大部分好像在DOM3文章中的方法都没有被实现或者废弃,所以下面作者给大家讲解的都是最新的事件叙述与DOM2定义差异,来自W3C的官方文档

DOM2到最新版本已经对接口和事件类型进行了许多说明。本文档中不再定义 HTML Events 模块。focus和blur事件已添加到 UIEvent 模块,dblclick 事件已添加到 MouseEvent 模块。这个新规范在 DOM 事件流、事件类型和 DOM 接口之间提供了更好的分离。

对 DOM 2 级事件流的更改

这个新规范在事件流中引入了以下新概念:

  • Event listeners现在拥有顺序概念,在DOM2时,没有顺序概念

  • 事件流现在包括window,以关联现有的实现。(在DOM2的定义中,捕获开始于document

对 DOM 2 级事件类型的更改

已经对事件类型进行了许多澄清。一致性现在是针对事件类型明确定义的,而不仅仅是事件类型使用的接口。

MutationEvents”已被弃用。对本规范早期草案中的命名空间事件的支持也已被删除。

对于支持 DOMNodeInsertedDOMNodeRemoved 事件类型的用户代理,本规范不再要求为 Attr 节点触发事件类型。

resize 事件类型不再冒泡,mousemove 事件现在可以取消,反映了现有的实现。

对 DOM Level 2 事件接口的更改

  • Interface Event

    1. Event 接口有一个新属性 defaultPrevented 和一个新方法 stopImmediatePropagation()

      defaultPrevented

      如果事件调用event.preventDefault() 那么就会是true

      stopImmediatePropagation()

      Event 接口的 stopImmediatePropagation()  方法阻止监听同一事件的其他事件监听器被调用。

      如果多个事件监听器被附加到相同元素的相同事件类型上,当此事件触发时,它们会按其被添加的顺序被调用。如果在其中一个事件监听器中执行 stopImmediatePropagation() ,那么剩下的事件监听器都不会被调用。

    2. timeStamp 现在是 ECMAScript 绑定中的数字。即将在 [DOM-Level-3-Core] 中进行相同更改的建议更正。

    3. 本规范认为 type 属性区分大小写,而 DOM Level 2 Events 认为 type 不区分大小写。

  • Interface EventTarget

    方法 dispatchEvent() 发生改变(这里因为作者只能找到最新的定义所以不清楚DOM2-DOM3之间发生什么变化)

  • Interface MouseEvent

    MouseEvent 接口有一个新方法 getModifierState()

  • Exception EventException

    本规范中删除了异常 EventException。之前的 DISPATCH_REQUEST_ERR 代码被重新映射到 InvalidStateError 类型的 DOMException。

新增的接口对象

interfaces CustomEventFocusEventKeyboardEventCompositionEvent, and WheelEvent 

这里我仅介绍CustomEvent对象,其他对象大家可以点击文档自行查看

CustomEvent

在最新的DOM文档介绍中CustomEvent是一个全局的构造函数对象,详细定义如下:


interface CustomEvent : Event {
  constructor(DOMString type, optional CustomEventInit eventInitDict = {});

  readonly attribute any detail;

  undefined initCustomEvent(DOMString type, optional boolean bubbles = false, optional boolean cancelable = false, optional any detail = null); // legacy
};

dictionary CustomEventInit : EventInit {
  any detail = null;
};

使用案例如下:

const obj = new EventTarget()
// add an appropriate event listener
obj.addEventListener("cat", function(e) { process(e.detail) })

// create and dispatch the event
var event = new CustomEvent("cat", {"detail":{"hazcheeseburger":true}})
obj.dispatchEvent(event)

那么通过上述内容CustomEvent可以自定义事件,并且为事件添加自定义信息,在浏览器端进行事件的派发.

但是如果您的项目需要兼容IE。那对不起不支持,点击这里可以查看兼容性 image.png

那么到这里浏览器的事件机制基本上就介绍完了,接下来我们看看Node的事件机制

Node的事件机制

作者通读官方,做了大致总结,首先Node的官方事件管理包含以下:

  • EventEmitter
  • Event
  • EventTarget
  • NodeEventTarget

那么他们包含以下几个特点:

  • 同一事件的监听器是有序排列的
  • listener函数中this关键字指向事件管理器
  • 事件管理器对于同步事件的错误有单独的事件派发error
  • 事件管理器对于Promise.reject可以通过Symbol.for('nodejs.rejection')或者error来进行错误事件派发

接着,我们来了解一下EventTarget类 与 DOM EventTarget有什么区别:

  1. 尽管 DOM EventTarget 实例可能是分层的,但 Node.js 中没有层次和事件传播的概念。 也就是说,调度到 EventTarget 的事件不会通过嵌套目标对象的层次结构传播,这些目标对象可能每个都有自己的事件句柄集(不存在事件流的概念)。
  2. 在 Node.js EventTarget 中,如果事件监听器是异步的函数或者返回 Promise,并且返回的 Promise 拒绝,则该拒绝会被自动捕获并按照同步抛出的监听器的方式处理(详见 EventTarget 错误处理)。

了解完上述信息以及概念后,我们来了解EventEmitter的作用:

EventEmitter 类

因为官方的介绍不多,所以我就自己按照想法说了,我认为EventEmitter实际上是个事件中心,包含部分的固有事件管理,那么其中固有事件包括newListener(监听器新增事件),removeListener(监听器移除事件),error

它包括以下方法:

我们可以发现,其实事件中心的设计想法主要点:

  • 数组操作
  • 限流执行
  • Event
  • EventTarget
  • NodeEventTarget类 我个人认为与DOM的差异并不是很大,大部分API小伙伴可以通过阅读文档进行学习。但是要注意以下:

EventTarget 的错误处理

当注册的事件监听器抛出错误(或返回拒绝的 Promise)时,默认情况下,错误将被视为 process.nextTick() 上的未捕获异常。 这意味着 EventTarget 中未捕获的异常将默认终止 Node.js 进程。

在事件监听器中抛出错误不会阻止其他注册的句柄被调用。

EventTarget 没有为 'error' 类型的事件(如 EventEmitter)实现任何特殊的默认处理。

当前错误在到达 process.on('uncaughtException') 之前首先转发到 process.on('error') 事件。 此行为已弃用,并将在未来版本中更改,以使 EventTarget 与其他 Node.js API 保持一致。 任何依赖 process.on('error') 事件的代码都应与新行为保持一致。

NodeEventTarget 对比 EventEmitter

NodeEventTarget 对象实现了 EventEmitter API 的修改子集,允许它在某些情况下接近地模拟 EventEmitter。 NodeEventTarget 不是 EventEmitter 的实例,在大多数情况下不能代替 EventEmitter

  1. 与 EventEmitter 不同,任何给定的 listener 最多可以在每个事件 type 中注册一次。 尝试多次注册 listener 将被忽略。
  2. NodeEventTarget 不模拟完整的 EventEmitter API。 特别是 prependListener()prependOnceListener()rawListeners()setMaxListeners()getMaxListeners() 和 errorMonitor API 没有被模拟。 'newListener' 和 'removeListener' 事件也不会触发。
  3. NodeEventTarget 没有为类型为 'error' 的事件实现任何特殊的默认行为。
  4. NodeEventTarget 支持 EventListener 对象以及作为所有事件类型句柄的函数。

那么关于事件我们就学习到这里!~

完美!

下一章: 面试准备-异步