JavaScript 事件(Events)

1,771 阅读32分钟

JavaScript经常要与HTML进行各种各样的交互,这种交互是通过事件实现的,那么什么是事件呢?

简单来说,事件(event) 就是发生在 Web 浏览器中的操作,Web 浏览器将其反馈给用户,以便用户对其做出响应。

每个事件都可以有一个事件处理程序(event handler),这是一个代码块,当事件发生的时候将会执行。事件处理程序也称为事件监听器(event listener),它监听事件并在事件发生时执行。

一、事件流(Event Flow)

应用程序可以使用dispatchEvent()方法来调度事件对象,事件对象将按照DOM事件流决定的方式在DOM树中传播。 事件对象被分派到事件目标之前,必须首先确定事件对象的传播路径

传播路径:是事件所经过的当前事件目标的一个有序列表。这个传播路径反映了文档的分层树形结构。列表中的最后一项是事件目标,列表中前面的项被称为目标的祖先,紧接着的项是目标的父辈

一旦确定了传播路径,事件对象就会经过一个或多个事件阶段。共有三个事件阶段:捕获阶段、目标阶段和冒泡阶段。

  • 捕获阶段(capture phase):事件对象通过目标的祖先从 Window 传播到目标的父级。
  • 目标阶段(target phrase):事件对象到达事件目标。如果事件类型表明事件没有冒泡,则事件对象将在此阶段完成后停止。
  • 冒泡阶段(bubble phrase):事件对象以相反的顺序通过目标的祖先传播,从目标的父级开始,到 window 结束。 事件对象如上所述完成这些阶段。如果不支持某个阶段,或者事件对象的传播已停止,则该阶段将被跳过。例如,如果 bubbles 属性设置为 false,则将跳过冒泡阶段,如果在调度之前调用了 stopPropagation(),则将跳过所有阶段

DOM事件流如下图所示: image.png

上述有关事件流的描述来自 W3C

二、事件处理程序(Event Handler)

事件意味着用户或浏览器执行的某种动作,比如单击(click)、加载(load)、滚动(scroll)等。为响应事件而调用的函数称为事件处理程序(或事件监听器(Event Listener))。事件处理程序的名字通常以 "on" 开头。
那么有哪些方法来定义事件处理程序呢,或者说为事件添加监听器呢?

2.1 HTML Attribute 事件处理程序

特定元素支持的每个事件都可以使用事件处理程序的名字以 HTML 特性(attribute) 的形式来指定。此时属性的值必须是能够执行的 JavaScript 代码。即在 HTML 标签的名为 on<event> 的 attribute 中。例如:

  • JavaScript可执行代码
 <button onclick="console.log('clicked!')">点击</button>
  • 调用 <script> 标签 或 外部JS文件中定义的函数
<button onclick="onClick()">点击</button>

<script>
    function onClick() {
        console.log('clicked!');
    }
</script>

注意\color{red}{注意}:HTML 标签中的 on<event> 特性的值是可执行的JavaScript代码,所以调用其他地方定义的函数时,要写完整的函数调用,而不是传递函数名。

另外,以这种方式定义的事件处理程序需要注意的是:

(1)event对象

以这种方式定义的事件处理程序会首先创建一个函数来封装属性的值,这个函数中有一个特殊的局部变量 event,其中保存的是 event 对象。

<button onclick="console.log(event);">点击</button>

image.png

(2)this值

由(1)可知,属性值中的可执行代码会被封装在一个函数中,那么由谁来调用这个函数呢?我们先看如下代码

<button class="test" onclick="console.log(this)">点击</button>

当我们点击按钮时,控制台输出的是<button class="test" onclick="console.log(this)">点击</button>,即按钮对象所以调用该函数的是事件所发生的对象。实际调用的函数如下所示:

ƒ onclick(event) {
    console.log(this.onclick)
}

但是,当属性值中是引用<script>或者外部JS文件中的函数时,输出this值时会有所不同,我们看如下代码:

<button class="test" onclick="onClick()">点击</button>

<script>
    function onClick() {
        console.log(this);
    }
</script>

当我们点击按钮时,控制台输出的并不是按钮对象,而是 Window 对象,这是为什么呢?实际上封装属性值得函数还是按钮对象调用的,封装属性的函数如下代码所示:

ƒ onclick(event) {
    onClick()
}

但是执行onClick()时并没有任何对象调用,所以该函数内部的this值在非严格模式下指向全局对象

此外,需要注意的是标签的on<event>属性名是忽略大小写的,但是内部封装属性值函数的名字是确定的,例如点击事件为onclick

2.2 DOM Property 事件处理程序

在 JavaScript 脚本中把一个函数赋值给DOM元素的事件处理程序属性。一般先获取DOM元素对象,再给对应属性赋值,如下所示:

let btn = document.querySelector('.test');  // 获取DOM元素
btn.onclick = function() {
    console.log('clicked!');
}

注意,赋给属性的值是一个函数,所以等号右边是函数,而不是函数调用。与1.1中的JavaScript可知执行代码有所不同。

let btn = document.querySelector('.test');

btn.onclick  = clickHandler;
function clickHandler(){
    console.log('clicked!');
}

这种方式设置的事件处理程序函数中的this值,若是函数表达式或函数名,则指向DOM元素的。箭头函数则指向 Window

let btn = document.querySelector('.test');
btn.onclick = function() {
    console.log(this);  // <button class="test" onclick="console.log(this.onclick)">点击</button>
}

btn.onclick = () => {
    console.log(this);  // Window
}

当设置了多个 DOM property 时,只有最后一个会生效,后面的会替换掉前面的。如下代码,当点击按钮时,控制台只会输出"after"。

let btn = document.querySelector('.test');

btn.onclick = function() {
    console.log('before');  
}
btn.onclick = function() {
    console.log('after');  
}

如果想删除该事件处理程序,就将该属性值设置为null(HTML事件处理程序也可通过该方式删除)

btn.onclick = null;

另外,这里的属性名都是小写的,不能忽略大小写

2.3 addEventListener() 和 removeEventListener()

(1)EventTarget.addEventListener()

EventTarget.addEventListener() 方法将指定的事件监听器(Event Listener) 注册到事件目标(EventTarget) 上,当该对象触发指定的事件时,指定的回调函数就会被执行。

addEventListener()的工作原理是将实现 EventListener 的函数或对象添加到调用它的 EventTarget 上的指定事件类型的事件监听器列表

语法

target.addEventListener(type, listener, options);
target.addEventListener(type, listener, useCapture);

参数

  • type:表示监听事件类型的字符串
  • listener:事件监听器(或事件处理程序),一个实现了 EventListener 接口的对象,或者是一个函数
  • option(可选项):一个指定有关 listener 属性的可选参数对象
    • capture:  Boolean,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
    • once:  Boolean,表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除。(例如点击事件,只会在第一次点击鼠标才会生效,后续点击都不起作用)
    • passive: Boolean,设置为true时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。
    • signalAbortSignal,该 AbortSignal 的 abort() 方法被调用时,监听器会被移除。
  • useCapture(可选):Boolean,在DOM树中,注册了listener的元素, 是否要先于它下面的EventTarget,调用该listener。 当useCapture(设为true) 时,沿着DOM树向上冒泡的事件,不会触发listener。当一个元素嵌套了另一个元素,并且两个元素都对同一事件注册了一个处理函数时,所发生的事件冒泡和事件捕获是两种不同的事件传播方式。事件传播模式决定了元素以哪个顺序接收事件。

示例

  • listener 参数的几种形式 以及 对应的 this listener参数可以设置为如下几种形式:
  1. 回调函数
    1. 函数名
    2. 函数表达式
    3. 箭头函数
  2. 实现 EventListener的对象,其handleEvent() 方法用作回调函数。 回调函数接受一个参数:一个基于Event 的对象,描述已发生的事件,并且它不返回任何内容。
let btn = document.querySelector('.test');

// 通过函数名设置listener
function clickHandler(event) {
    console.log(this);  // Button
}
btn.addEventListener('click', clickHandler)

// 通过匿名函数设置listener
btn.addEventListener('click', function(event) {
    console.log(this);  // Button
})

// 通过箭头函数设置listener
btn.addEventListener('click', (event) => {
    console.log(this);  // Window
})

// 通过实现了 EventListener 的对象设置listener
btn.addEventListener('click', {
    handleEvent: function (event) {
        console.log(this);  // {handleEvent: ƒ}
    }
});

如上述代码所示:通过函数名和函数表达式设置的listener参数,事件处理程序中的this值为事件目标元素,箭头函数中的this值则是Window,实现 EventListener的对象的handleEvent()中的this则为该对象。

  • option对象的的使用 首先给出例子的HTML和CSS:
<style>
    .outer, .middle, .inner{
        display:block;
        padding:15px;
        margin:15px;
        text-decoration:none;
    }
    .outer{
        border:1px solid red;
        color:red;
        width:300px;
    }
    .middle{
        border:1px solid green;
        color:green;
        width:200px;
    }
    .inner{
        border:1px solid purple;
        color:purple;
        width:100px;
    }
</style>

<body>
    <div class="outer">
        outer
        <div class="middle">
            middle
            <div class="inner">
                inner
            </div>
        </div>
    </div>
</body>

image.png

  1. capture属性:表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
    假设我给outer添加了如下listener:
outer.addEventListener('click', () => {
    console.log('outer');
}, {once: true});

当我第一次点击outer时,控制台会输出"outer",但是当我第二次点击时,没有任何反应。这是因为设置了{once: true},导致第一次点击outer调用了监听器后,就会将该监听器删除。所以,只有第一次点击的时候才会触发监听器。
此外,如果是捕获阶段或者冒泡阶段监听器被触发了,设置了{once: true}的监听器也会在第一次触发后被删除。

  1. capture属性: 表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。 假设我添加了如下listener:
outer.addEventListener('click', () => {
    console.log('outer');
});

middle.addEventListener('click', () => {
    console.log('middle');
}, {capture: true});

inner.addEventListener('click', () => {
    console.log('inner');
});

当我们点击inner的时候,控制台会如下输出:

middle
inner
outer

设置了{capture: true}说明会在捕获阶段被调用,如第一节中有关事件流的介绍我们可知,捕获阶段是从 Window 到事件目标的父元素,所以inner的父元素middle监听器会在inner的监听器之前调用。祖先元素的监听器默认是在冒泡阶段被调用的。
此外,设置useCapture参数为true和设置{capture: true}作用相同

  1. passive属性: 设置为true时,表示 listener 永远不会调用 preventDefault() 。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。 假设我添加了如下listener:
inner.addEventListener('contextmenu', (event) => {
    event.preventDefault();
    console.log('inner');
}, {passive: true});

当我点击右击inner时,仍然会出现菜单栏,而且控制台还会输出 "Unable to preventDefault inside passive event listener invocation."

  • 添加多个事件监听器 addEventListener可以给一个事件添加多个监听器,但同一个 EventTarget 注册了多个相同的 EventListener,那么重复的实例会被抛弃。如果capture的值不一致,此时就算重复定义,也不会被抛弃掉。

这段代码中,给同一个事件添加了相同的监听器,重复的会被抛弃。所以点击outer时,控制台只会输出一个"outer"

outer.addEventListener('click', handleClick);
outer.addEventListener('click', handleClick);
function handleClick() {
    console.log('outer');
}

但是下面这段代码中,监听器虽然相同,但是被添加到不同的阶段,所以不会被抛弃。所以点击outer时,控制台只会输出两个"outer"

outer.addEventListener('click', handleClick);
outer.addEventListener('click', handleClick, {capture: true});
function handleClick() {
    console.log('outer');
}

最后还需要注意的是,如果是通过匿名函数箭头函数添加的事件监听器,即使实现的功能以及代码相同,也是添加的不同的监听器。如下代码,点击outer时,控制台只会输出两个"outer"

outer.addEventListener('click', () => {
    console.log('outer');
});

outer.addEventListener('click', () => {
    console.log('outer');
});

function handleClick() {
    console.log('outer');
}
  • 在事件分派时添加事件处理器 当一个 EventListener 在 EventTarget 正在处理事件的时候被注册到 EventTarget 上,它不会被立即触发,但可能在事件流后面的事件触发阶段被触发,例如可能在捕获阶段添加,然后在冒泡阶段被触发。

(2)addEventListener() 的优势

  • 它允许给一个事件注册多个监听器。 特别是在使用AJAX库,JavaScript模块,或其他需要第三方库/插件的代码。
  • 它提供了一种更精细的手段控制 listener 的触发阶段。即可以选择捕获阶段或冒泡阶段。
  • 它对任何 DOM 元素都是有效的,而不仅仅只对 HTML 元素有效。
  • 另外,个人认为这样比较符合JavaScript函数式编程的思想。

(3)EventTarget.removeEventListener()

删除使用 EventTarget.addEventListener() 方法添加的事件。使用事件类型,事件侦听器函数本身,以及可能影响匹配过程的各种可选择的选项的组合来标识要删除的事件侦听器。

  • 一个 EventTarget上的 EventListener被移除之后,如果此事件正在执行,会立即停止。 EventListener 移除之后不能被触发,但可以重新绑定。

  • EventTarget上使用任何未识别当前注册的EventListener调用 removeEventListener() 不会起任何作用。

语法:

target.removeEventListener(type, listener[, options]);
target.removeEventListener(type, listener[, useCapture]);

参数

  • type: 一个字符串,表示需要移除的事件类型,如 "click"
  • listener: 需要从目标事件移除的 EventListener函数。
  • options(可选):一个指定事件侦听器特征的可选对象。可选项有:
    • capture: 一个 Boolean, 表示这个类型的事件将会被派遣到已经注册的侦听器,然后再派遣到DOM树中它下面的任何 EventTarget
  • useCapture(可选):指定需要移除的 EventListener 函数是否为捕获监听器。如果无此参数,默认值为 false

示例

  • 如果同一个监听事件分别为“事件捕获”和“事件冒泡”注册了一次,这两次事件需要分别移除。两者不会互相干扰。如下代码所示,要分别移除:
middle.addEventListener('click', handleClick, true);
middle.addEventListener('click', handleClick);

middle.removeEventListener('click', handleClick)
middle.removeEventListener('click', handleClick, true)
  • removeEventListener()的参数option中只能设置capture,当添加监听器时设置了oncepassive,移除时不需要设置对应属性.
middle.addEventListener('click', handleClick, {passive: true});

middle.removeEventListener('click', handleClick)

// 当然这种写法也可以移除
// middle.removeEventListener('click', handleClick, {passive: true})

此外,如果某个监听器未来可能被移除,那么添加的时候最好不要使用匿名函数表达式或箭头函数的方式。最好保持函数引用。

HTML Attribute、DOM Property、addEventListener() 之间的关系

    • HTML attribute 方式 和 DOM property 方式共同存在时,一般只有DOM property 会生效。如下代码,点击按钮后控制台只会输出"DOM"。
<button class="test" onclick="console.log('HTML')">点击</button>

<script>
    let btn = document.querySelector('.test');

    btn.onclick = function() {
        console.log('DOM');  
    }
</script>
  • addEventListener() 不会和 HTML attribute 和 DOM property 发生冲突,因为addEventListener()可以添加多个事件监听。
<button class="test" onclick="console.log('HTML')">点击</button>

<script>
    let btn = document.querySelector('.test');

    btn.addEventListener('click', () => {
        console.log('addEventListener');
    });
</script>

上述代码中,点击按钮会依次输出"HTML"和"addEventListener"。

  • addEventListener() 添加的事件监听器在和 HTML attribute 和 DOM property 之后被触发。如下代码,点击按钮,控制台会先输出"DOM", 再输出"addEventListener"。
btn.addEventListener('click', () => {
    console.log('addEventListener');
});

btn.onclick = function() {
    console.log('DOM');  
}

2.4 传统 IE 事件处理程序

对于 Internet Explorer 来说,在IE 9之前,IE实现了attachEvent()detachEvent() 两个方法来添加和移除事件监听器。

所以,为了支持IE,添加事件监听器的代码可能要写成如下形式:

if (el.addEventListener) {
  el.addEventListener('click', modifyText, false);
} else if (el.attachEvent)  {
  el.attachEvent('onclick', modifyText);
}

使用 attachEvent 会存在以下缺点:

  • IE8 以及更早版本只支持事件冒泡,所以使用 attachEvent 添加的事件处理程序会添加到冒泡阶段。
  • this 的值会变成 window 对象的引用而不是触发事件的元素。 IE浏览器将于2022年6月16日正式退役了,个人感觉attachEvent()detachEvent()在未来可能都不会再使用了,大家了解一下就行。

2.5 跨浏览器事件处理程序(兼容性)

为了解决浏览器的兼容性问题,可自定义如下添加监听器和移除监听器的方法。

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

三、事件对象(Event)

在DOM中发生事件时,所有相关信息都会被收集并存储到一个名为 event 的对象中,这个对象包括了一些基本信息,例如导致事件的元素、发生事件的类型,以及可能与特定事件相关的任何其他数据。
event 对象时传给事件处理程序的唯一参数

3.1 Event() 构造函数

Event()  构造函数, 创建一个新的事件对象 Event

 event = new Event(typeArg, eventInit);
  • typeArg:是DOMString 类型,表示所创建事件的名称。
  • eventInit可选:是 EventInit 类型的字典,接受以下字段:
    • "bubbles",可选,Boolean类型,默认值为 false,表示该事件是否冒泡。
    • "cancelable",可选,Boolean类型,默认值为 false, 表示该事件能否被取消。
    • "composed",可选,Boolean类型,默认值为 false,指示事件是否会在影子DOM根节点之外触发侦听器。

3.2 Event 对象属性

属性类型读/写说明
bubblesBoolean只读表明当前事件是否会向DOM树上层元素冒泡
cancelableBoolean只读表明事件是否可以被取消,即事件是否可以向从未发生一样被阻止
composedBoolean只读表明事件是否可以从Shadow DOM 传递到一般的DOM
currentTarget元素只读表明当前事件处理程序所在的元素
target元素只读表明触发事件的对象 (某个DOM元素) 的引用
defaultPreventedBoolean只读表明当前事件是否调用了 event.preventDefault()方法
eventPhrase整数只读表明事件流当前处于哪一个阶段。捕获阶段(1),目标阶段(2),冒泡阶段(3)
isTrustedBoolean只读当事件是由用户行为生成的时候,这个属性的值为 true ,而当事件是由脚本创建、修改、通过 EventTarget.dispatchEvent() 派发的时候,这个属性的值为 false 。
timeStamp数值只读表明事件发生时的时间戳(在Gecko中,该属性的值不是事件发生时正确的事件戳)
type字符串只读表示该事件对象的事件类型

currentTargettarget 的区别

我们仍以下面这个界面为例:

image.png

假设我们对上述元素添加以下事件监听器:

let outer  = document.querySelector('.outer');
let middle = document.querySelector('.middle');
let inner = document.querySelector('.inner');

outer.addEventListener('click', function(event){
    console.log('outer currentTarget: ', event.currentTarget);
    console.log('outer target: ', event.target);
});
middle.addEventListener('click', function(event){
    console.log('middle currentTarget: ', event.currentTarget);
    console.log('middle target: ', event.target);
});
inner.addEventListener('click', function(event){
    console.log('inner currentTarget: ', event.currentTarget);
    console.log('inner target: ', event.target);
});

当我们分别点击inner 、middle、outer 控制台输出如下:

  • 点击 inner

image.png

  • 点击 middle

image.png

  • 点击 outer

image.png

从上述结果可以看出,currentTarget 永远指的是事件监听器所注册的事件对象,而 target 则指的是触发事件的元素,如我们分别点击 inner、middle、outer 时,target 值都有所不同。

3.3 Event 对象的方法

(1)Event.composedPath()

当对象数组调用该监听器时返回事件路径。 如果影子根节点被创建并且ShadowRoot.mode是关闭的,那么该路径不包括影子树中的节点。

var composed = Event.composedPath();

一个 EventTarget对象数组,表示将在其上调用事件侦听器的对象。

(2)event.preventDefault()

取消事件的默认行为

event.preventDefault();

例如:取消鼠标右击默认弹出的菜单

btn.addEventListener('contextmenu', function(event){
    event.preventDefault();
});

(3) event.stopPropagation()

stopPropagation() 方法阻止捕获和冒泡阶段中当前事件的进一步传播。但是不会阻断该对象上注册的其他监听器。

event.stopPropagation();

如下代码所示,点击 inner 时,控制台只会输出"inner","inner2",后面冒泡阶段的事件被阻断了,不会触发了。

outer.addEventListener('click', function(event){
    console.log('outer');
});
middle.addEventListener('click', function(event){
    console.log('middle');
});
inner.addEventListener('click', function(event){
    console.log('inner1');
    event.stopPropagation();
});
inner.addEventListener('click', function(event){
    console.log('inner2');
});

(4)event.stopImmediatePropagation()

stopImmediatePropagation() 方法阻止监听同一事件的其他事件监听器被调用。(不仅阻止了当前元素上该事件的其他监听器,也阻止了事件流的传播)

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

event.stopImmediatePropagation();

如下代码,控制台只会输出"inner",

outer.addEventListener('click', function(event){
    console.log('outer');
});
middle.addEventListener('click', function(event){
    console.log('middle');
});
inner.addEventListener('click', function(event){
    console.log('inner1');
    event.stopImmediatePropagation();
});
inner.addEventListener('click', function(event){
    console.log('inner2');
});

四、事件类型

关于所有的事件类型,可以参考MDN事件参考,这里介绍一些常见的事件类型。

  • 用户界面事件(UIEvent):涉及与DOM交互的通用浏览器事件。
  • 焦点事件(FocusEvent):元素获得和失去焦点时触发。
  • 鼠标事件(MouseEvent):使用鼠标在页面上执行某些操作时触发。
  • 滚轮事件(ScrollEvent):使用鼠标滚轮(或类似设备)时触发。
  • 输入事件(InputEvent):向文档中输入文本时触发。
  • 键盘事件(KeyboardEvent):使用键盘在页面上执行某些操作时触发。
  • 合成事件(CompositionEvent):使用某种IME(Input Method Editor)输入字符时触发。

4.1 用户界面事件

(1)load 事件

整个页面及所有依赖资源(如图片、JavaScript 文件和 CSS 文件)都已完成加载时,将触发load事件。

可通过以下两种方式设置:

// addEventListener() 添加
window.addEventListener('load', (event) => {
    ...
});

// DOM Property 添加
window.onload = (event) => {
  ...
};

注意:事件名为 load, 而属性名为 onload

(2)beforeunload 事件

当浏览器窗口关闭或者刷新时,会触发beforeunload事件。当前页面不会直接关闭,可以点击确定按钮关闭或刷新,也可以取消关闭或刷新。

但需要注意以下两点:

  • 显示确认对话框,事件处理程序需要在事件上调用preventDefault()
  • 并非所有浏览器都支持此方法,有些浏览器需要事件处理程序将字符串分配给事件的returnValue属性
  • 将字符串分配给事件的returnValue属性 如下代码:
window.addEventListener('beforeunload', (event) => {
    // Cancel the event as stated by the standard.
    event.preventDefault();
    // Chrome requires returnValue to be set.
    event.returnValue = '';
});

(3)unload 事件

当文档或一个子资源正在被卸载时, 触发 unload事件。它在 beforeunloadpagehide 事件之后被触发。

文档处于以下状态:

  • 所有资源仍存在 (图片, iframe 等.)
  • 对于终端用户所有资源均不可见
  • 界面交互无效 (window.openalertconfirm 等.)
  • 错误不会停止卸载文档的过程 但是 MDN 中不推荐使用该事件,

(4)abort 事件

资源没有被完全加载时就会触发 abort 事件,但错误不会触发该事件。

(5)error 事件

当一个资源加载失败或无法使用时,会在Element对象上触发error事件。例如当脚本执行错误、或图片无法找到或图片无效时。

(6)resize 事件

文档视图调整大小时会触发 resize 事件。

(7)scroll 事件

文档视图或者一个元素在滚动时,会触发元素的**scroll事件。

(8)fullscreenchange 事件

fullscreenchange事件当浏览器进入或离开全屏时触发。

(9)Document.onfullscreenerror 事件

Document.onfullscreenerror 属性是一个事件处理器用于处理 fullscreenchange 事件,在当前文档不能进入全屏模式,即使它被请求时触发。

4.2 焦点事件

(1)获取焦点事件

  • focus 事件:元素获取焦点时触发,不会冒泡
  • focusin 事件:当元素即将获得焦点时触发,会冒泡

(2)失去焦点事件

  • blur 事件:元素失去焦点时触发,不会冒泡
  • focusout 事件:当元素即将失去焦点时触发,会冒泡

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

  1. focusout 在失去焦点的元素上触发
  2. focusin 在获得焦点的元素上触发
  3. blur 在失去焦点的元素上触发
  4. focus 在获得焦点的元素上触发

4.3 点击事件

事件名称触发
click在元素上按下并释放鼠标主键(通常是左键)。
contextmenu 右键点击(在右键菜单显示前触发)。
dblclick在元素上双击鼠标按钮。
mousedown 在元素上按下任意鼠标按钮。
mouseenter指针移到有事件监听的元素内。这个事件不冒泡,也不会在光标经过候到元素时触发
mouseleave指针移出元素范围外。这个事件不冒泡,也不会在光标经过候到元素时触发
mousemove指针在元素内移动时持续触发
mouseover指针移到有事件监听的元素或者它的子元素内。
mouseout指针移出元素,或者移到它的子元素上
mouseup在元素上释放任意鼠标按键。
wheel滚轮向任意方向滚动
----------。

(1)mousedown、mouseup、click、dblclick 事件的顺序

如果一个元素该四个事件都注册了监听器,那么双击该元素时,会按如下顺序触发:

  1. mousedown
  2. mouseup
  3. click
  4. mousedowm
  5. mouseup
  6. click
  7. dblclick

(2)mouseover 和 mouseenter

  • mouseover:当指针从外部元素移入原始元素 或者 从原始元素移入原始元素的子元素、从原始元素的子元素移入原始元素、从原始元素的子元素移入原始元素的孙元素等 都会触发
  • mouseenter: 只有在指针从外部元素移入当前元素,才会被触发 mouseover 事件 会在 mouseenter 事件之前被触发。

(3)mouseout 和 mouseleave

  • mouseout:当指针原始元素移出到外部元素 或者 从原始元素移出到原始元素的子元素、从原始元素的子元素移出到原始元素的,从原始元素的子元素移出到原始元素的孙元素等 都会触发
  • mouseleave: 只有在指针从当前元素移出到外部元素,才会被触发 mouseout 事件 会在 mouseleave 事件之前被触发。

image.png 假设对上图页面添加如下JavaScript代码:

outer.addEventListener('mouseenter', function(event){
    console.log('mouseenter');
});
outer.addEventListener('mouseleave', function(event){
    console.log('mouseleave');
});
outer.addEventListener('mouseout', function(event){
    console.log('mouseout');
});
outer.addEventListener('mouseover', function(event){
    console.log('mouseover');
});
  • 指针从 outer 外部移到 outer,会依次触发mouseovermouseenter事件
  • 指针从 outer 移动到 outer 外部,会依次触发 mouseoutmouseleave 事件
  • 如下操作都会依次触发 mouseoutmouseover 事件
    • 指针从 outer 移动到 middle
    • 指针从 middle 移动到 inner
    • 指针从 inner 移动到 middle
    • 指针从 middle 移动到 outer

(4)mousemove

只要鼠标在注册事件的元素内移动,都会触发,不管是否是在其子元素上。例如上述的例子,只要指针在 outer 的方框内移动,都会触发 mousemove 事件。

(5)wheel

  • WheelEvent.deltaX:只读 返回double值,该值表示滚轮的横向滚动量。

  • WheelEvent.deltaY:只读 返回double值,该值表示滚轮的纵向滚动量。

  • WheelEvent.deltaZ:只读 返回double值,该值表示滚轮的z轴方向上的滚动量。

  • WheelEvent.deltaMode: 只读 返回unsigned long值,该值表示上述各delta*的值的单位。该值及所表示的单位如下:

    常量描述
    DOM_DELTA_PIXEL0x00滚动量单位为像素。
    DOM_DELTA_LINE0x01滚动量单位为行。
    DOM_DELTA_PAGE0x02滚动量单位为页。

(6)事件对象中的坐标信息

1. 客户端坐标(clientX, clientY)

以视口区域左上角为原点建立的坐标系。即鼠标光标在视口区域上的位置。

2. 页面坐标(pageX, pageY)

鼠标指针在页面上的位置,指的是整个文档,而不是视口那一块。当没有滚动时,(pageX, pageY) 和 (clientX, clientY)相同。

3. 屏幕坐标(screenX, screenY)

以电脑屏幕左上角为原点建立的坐标系。即鼠标光标在电脑屏幕上的位置。

4. 鼠标移动值(movementX, movementY)

它提供了当前事件和上一个mousemove事件之间鼠标在水平和垂直方向上的移动值。换句话说,这个值是这样计算的 : 

  • currentEvent.movementX = currentEvent.screenX - previousEvent.screenX
  • currentEvent.movementY = currentEvent.screenY - previousEvent.screenY

(7)修饰键

虽然鼠标事件主要是通过鼠标触发的,但如果用户想结合键盘来触发,可以使用event对象中提供的修饰键。键盘上的修饰键 Shift、Ctrl、Alt 和 Meta,DOM规定了四个属性:shiftKeyctrlKeyaltKeymetaKey 表示修饰键的状态。如果按下则为true.

如下例子中,只有同时按住Alt键 并且 点击鼠标左键 才会触发事件,控制台才会输出123.

outer.addEventListener('click', function(event){
    if (event.altKey){
        console.log('123')
    }
});

(8)detail 属性

UIEvent.detail是只读属性, 当值为非空的时候, 提供当前点击数(和环境有关) 。

  • 对 click 或者 dblclick 事件, UIEvent.detail 是当前点击数量
  • 对 mousedown 或者 mouseup  事件, UIEvent.detail是1加上当前点击数

(9)relatedTarget

只读属性 MouseEvent.relatedTarget 是鼠标事件的次要目标(如果存在),它包括:

事件名称targetrelatedTarget
focusin (en-US)EventTarget 获取焦点EventTarget 失去焦点
focusout (en-US)EventTarget 失去焦点The EventTarget 获取焦点
mouseenter (en-US)指针设备进入EventTarget指针设备离开EventTarget
mouseleave (en-US)指针设备离开 EventTarget指针设备进入 EventTarget
mouseout (en-US)指针设备离开 EventTargetThe EventTarget
mouseover (en-US)指针设备进入 EventTarget指针设备离开 EventTarget
dragenter (en-US)指针设备进入 EventTarget指针设备离开 EventTarget
dragexit指针设备离开 EventTarget指针设备进入 EventTarget

如果事件没有次要目标,relatedTarget 将返回 null.

4.4 键盘与键入事件

键盘事件是用户操作键盘时触发的,包含以下三个事件

事件名称触发
keydown按下任意按键。
keypress除 Shift、Fn、CapsLock 外的任意键被按住。(连续触发。)(已弃用)
keyup释放任意按键。

(1)KeyboardEvent.code

KeyboardEvent.code属性表示键盘上的物理键(与按键生成的字符相对)。换句话说,此属性返回一个值,该值不会被键盘布局或修饰键的状态改变。例如按键 S 对应的 code 为 KeyS

(2)KeyboardEvent.key

只读属性 KeyboardEvent.key 返回用户按下的物理按键的值。例如:按键 S 对应的 key 为 s,即键盘上标的时啥key就是啥。

code 属性 和 key 属性除了表示上不同,此外 code 还会与键盘布局相关,例如按下键盘左边的 Shift 键,code 值为 ShiftLeft, 而 key 为 Shift

(3)keyboardEvent.location

KeyboardEvent.location 是一个只读属性,返回一个无符号的长整型 unsigned long,表示按键在键盘或其他设备上的位置

(4)KeyboardEvent.repeat

KeyboardEvent.repeat是一个只读属性,返回一个布尔值Boolean,如果按键被一直按住,返回值为true

(5) KeyboardEvent.isComposing

KeyboardEvent.isComposing只读属性,返回一个 Boolean 值,表示该事件是否在 compositionstart 之后和 compositionend 之前被触发。

此外键盘事件对象也规定了四个属性:shiftKeyctrlKeyaltKeymetaKey 表示修饰键Shift、Ctrl、Alt 和 Meta的状态。如果按下则为true.这些可以与其他键组合设置一些快捷键。

4.5 合成事件

(1)compositionend

当文本段落的组成完成或取消时, compositionend 事件将被触发 (具有特殊字符的触发, 需要一系列键和其他输入, 如语音识别或移动中的字词建议)。

(2)compositionstart

文本合成系统如 input method editor(即输入法编辑器)开始新的输入合成时会触发 compositionstart 事件。

(3)compositionupdate

compositionupdate 事件触发于字符被输入到一段文字的时候(这些可见字符的输入可能需要一连串的键盘操作、语音识别或者点击输入法的备选词)

4.6 其他事件

(1)DOMContentLoaded

当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完全加载。

注意:设置了defer属性的script标签异步加载JavaScript文件,在HTML解析完之后去执行,但在DOMContentLoaded事件之前执行。

(2)hashchange

当URL的片段标识符更改时,将触发hashchange事件 (跟在#符号后面的URL部分,包括#符号)

上述值列举了一些常用的事件,但还有其他很多事件,具体可查看MDN官网。

五、内存与性能

在 JavaScript 中,页面中事件处理程序的数量与页面整体性能直接相关。原因大概如下:

  • 每个函数都是对象,都占用内存空间,对象越多,性能越差
  • 为指定事件处理程序所需访问 DOM 的次数会先期造成整个页面交互的延迟 ...

5.1 事件委托

(1)什么式事件委托

解决“事件处理程序过多”的一个方法就是事件委托,即利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件。

假设有一个如下的列表,点击列表不同项执行触发不同响应:

<ul class="test_ul">
    <li id="item1">列表项1</li>
    <li id="item2">列表项2</li>
    <li id="item3">列表项3</li>
</ul>

简单来说,我们可以为列表的每一项添加一个事件处理程序:

let item1 = document.getElementById('item1');
let item2 = document.getElementById('item2');
let item3 = document.getElementById('item3');

item1.addEventListener('click', () => {
    console.log('item1');
})
item2.addEventListener('click', () => {
    console.log('item2');
})
item3.addEventListener('click', () => {
    console.log('item3');
})

这样每个列表项都对应创建了一个函数对象,但如果使用事件委托,就可以减少事件处理函数对象的创建:

let ul = document.querySelector('.test_ul');
ul.addEventListener('click', () => {
    let target = event.target;
    switch(target.id) {
        case 'item1':
            console.log('item1');
            break;
        case 'item2':
            console.log('item2');
            break;
        case 'item3':
            console.log('item3');
            break;
    }
})

(2)事件委托的优点

  1. document 对象随时可用,任何时候都可以给它添加事件处理程序(不用等待 DOMContentLoaded 或 load 事件)。这意味着只要渲染出可点击的元素,就可以无延迟地起作用
  2. 更少的代码:添加或移除元素时,无需添加/移除处理程序。
  3. 节省了花在设置页面事件处理程序上的时间。
  4. 减少了整个页面所需的内存,提升整体性能。

(3)事件委托的缺点

事件委托确实节省了时间和内存,但也存在以下缺点:

  1. 事件必须冒泡。而有些事件不会冒泡。此外,低级别的处理程序不应该使用 event.stopPropagation()。 最合适使用事件委托的事件包括:clcik、mousedown、mouseup、keydown、keypress

  2. 其次,委托可能会增加 CPU 负载,因为容器级别的处理程序会对容器中任意位置的事件做出反应,而不管我们是否对该事件感兴趣。但是,通常负载可以忽略不计,所以我们不考虑它。

5.2 删除事件处理程序

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

导致这种情况的原因主要有两个:

  • 当删除带有事件处理的元素时,对应的事件处理程序不会被垃圾收集程序正常处理。
  • 页面加载事件残留,。如果在页面卸载后事件处理程序没有被清理,则它们仍然会残留在内存中。之后,浏览器每次加载和卸载页面(比如通过前进、后退或刷新),内存中残留对象的数量都会增加,这是因为事件处理程序不会被回收。 可以记住一点:onload 事件处理程序中做了什么,最好在 onunload 事件处理程序中恢复。

六、自定义事件

可以通过 EVent() 定义一些自定义事件,再使用 EventTarget.dispatchEvent() 方法将自定义事件派发往指定的目标(target)。

6.1 Event() 构造函数

Event()  构造函数, 创建一个新的事件对象 Event

语法

event = new Event(typeArg, eventInit);

参数

  • typeArg: 是DOMString类型,表示所创建事件的名称。
  • eventIni(可选):是 EventInit 类型的字典,接受以下字段:
    • "bubbles",可选,Boolean类型,默认值为 false,表示该事件是否冒泡。
    • "cancelable",可选,Boolean类型,默认值为 false, 表示该事件能否被取消。
    • "composed",可选,Boolean类型,默认值为 false,指示事件是否会在影子DOM根节点之外触发侦听器。

6.2 EventTarget.dispatchEvent

向一个指定的事件目标派发一个事件,  并以合适的顺序同步调用目标元素相关的事件处理函数。标准事件处理规则(包括事件捕获和可选的冒泡过程)同样适用于通过手动的使用dispatchEvent()方法派发的事件。

语法

cancelled = !target.dispatchEvent(event)

参数

  • event 是要被派发的事件对象。
  • target 被用来初始化 事件 和 决定将会触发 目标.

返回值

  • 当该事件是可取消的(cancelable为true)并且至少一个该事件的 事件处理方法 调用了Event.preventDefault(),则返回值为false;否则返回true

如果该被派发的事件的事件类型(event's type)在方法调用之前没有被经过初始化被指定,就会抛出一个 UNSPECIFIED_EVENT_TYPE_ERR 异常,或者如果事件类型是null或一个空字符串. event handler 就会抛出未捕获的异常; 这些 event handlers 运行在一个嵌套的调用栈中: 他们会阻塞调用直到他们处理完毕,但是异常不会冒泡。

同步调用

与浏览器原生事件不同,原生事件是由DOM派发的,并通过事件循环(event loop)异步调用事件处理程序,而dispatchEvent()则是同步调用事件处理程序。在调用dispatchEvent()后,所有监听该事件的事件处理程序将在代码继续前执行并返回。

dispatchEvent()是create-init-dispatch过程的最后一步,用于将事件调度到实现的事件模型中。可以使用 Event 构造函数来创建事件。

var event = new Event('build');

// Listen for the event.
elem.addEventListener('build', function (e) { 
    console.log('build');
 }, false);

// Dispatch the event.
elem.dispatchEvent(event);

自定义事件其实就是通过JavaScript代码来触发事件目标上注册的某个监听器,dispatchEvent(event)其实就类似于我们点击鼠标然后触发click事件处理程序。

关于事件的学习就先到这里啦,本文主要基于《JavaScript高级程序设计(第四版)》、MDN。如有错误,欢迎大家提出指正呀。

主要参考
[1] 《JavaScript高级程序设计(第四版)》
[2] EventTarget.addEventListener()
[3] Event
[4] 事件参考