### 彪哥

爱好前端

- 基础
- 内置对象
- 面向对象
- 异步操作
- DOM
- 事件
  - 一、EventTarget 接口
    - 1、概述
    - 2、EventTarget.addEventListener()
    - 3、EventTarget.removeEventListener()
    - 4、EventTarget.dispatchEvent()
  - 二、事件模型
    - 1、监听函数
    - 1.2 HTML 的 on- 属性
    - 1.2 元素节点的事件属性 (例：el.onclick)
    - 1.3 EventTarget.addEventListener()
    - 1.4 小结
    - 2、this 的指向 （指向元素节点）
    - 3、事件的传播（捕获、目标、冒泡）
    - 4、事件的代理
    - 阻止事件冒泡 event.stopPropagation()
  - 三、Event 对象
    - 1. 概述
    - 2. 实例属性
    - 2.1 Event.bubbles 是否会冒泡，只读，Event.eventPhase 返回整数，表示事件所处阶段，只读
    - 2.2 Event.cancelable 是否可取消默认行为，Event.cancelBubble是否阻止冒泡，event.defaultPrevented 是否调用过取消默认行为方法
    - 2.3 Event.currentTarget当前正在通过的节点，Event.target目标节点
    - 2.4 Event.type 事件类型（如：'click'）
    - 2.5 Event.timeStamp 相对于打开网页后的毫秒时间戳
    - 例子：计算鼠标移动速度
    - 2.6 Event.isTrusted 是否由真实用户行为产生的事件
    - 2.7 Event.detail 事件的细节（单击or双击等）
    - 3、实例方法
    - 3.1 Event.preventDefault() 取消浏览器对当前事件的默认行为
    - 例子：只能输入字母的输入框
    - 3.2 Event.stopPropagation() 阻止事件传播
    - 3.3 Event.stopImmediatePropagation() 阻止同一个事件的其他监听函数被调用
    - 3.4 Event.composedPath() 数组，目标和冒泡的节点
  - 四、鼠标事件
    - 1、鼠标事件的种类
    - 2、MouseEvent 接口概述
    - 3、MouseEvent 接口的实例属性
    - 3.1 MouseEvent.altKey，MouseEvent.ctrlKey，MouseEvent.metaKey，MouseEvent.shiftKey
    - 3.2 MouseEvent.button 鼠标的哪个键，MouseEvent.buttons同时按哪些键
    - 3.3 MouseEvent.clientX 相对浏览器X坐标，MouseEvent.clientY 相对浏览器Y坐标
    - 3.4 MouseEvent.movementX 上一个鼠标经过事件的X距离，MouseEvent.movementY 上一个鼠标经过事件的Y距离
    - 3.5 MouseEvent.screenX 相对屏幕X坐标，MouseEvent.screenY 相对屏幕Y坐标
    - 3.6 MouseEvent.offsetX 偏移量X，MouseEvent.offsetY 偏移量Y
    - 3.7 MouseEvent.pageX 文档X坐标，MouseEvent.pageY 文档Y坐标
    - 3.8 MouseEvent.relatedTarget 事件的相关节点
    - 4、MouseEvent 接口的实例方法
    - 4.1 MouseEvent.getModifierState() 是否按下指定功能键
    - 5、WheelEvent 接口 （滚轮）
    - 5.1 概述
    - 5.2 实例属性
  - 五、键盘事件
    - 1、键盘事件的种类
    - 2、KeyboardEvent 接口概述
    - 3、KeyboardEvent 的实例属性
    - 3.1 KeyboardEvent.altKey，KeyboardEvent.ctrlKey，KeyboardEvent.metaKey，KeyboardEvent.shiftKey 【是否按下对应键，布尔值】
    - 3.2 KeyboardEvent.code 键码
    - 3.3 KeyboardEvent.key 键名
    - 3.4 KeyboardEvent.location 键处于哪个位置，整数
    - 3.5 KeyboardEvent.repeat 是否长按
    - 4、KeyboardEvent 的实例方法
    - 4.1 KeyboardEvent.getModifierState() 是否按下指定功能键
  - 六、进度事件
    - 1、进度事件的种类
    - 2、ProgressEvent 接口
    - 2.1 概述
    - 2.2 ProgressEvent的实例属性。
  - 七、表单事件
    - 1、表单事件的种类
    - 1.1 input 事件 （值发生变化触发，会连续）
    - 1.2 select 事件 （选中文本时触发）
    - 1.3 change 事件 （值发生变化时触发，单次）
    - 1.4 invalid 事件 （表单提交不满足条件触发）
    - 1.5 reset 事件（重置），submit 事件（提交）
    - 2、InputEvent 接口（input事件的实例）
    - （1）InputEvent.data 变动的那部分内容
    - （2）InputEvent.inputType 变更类型
    - （3）InputEvent.dataTransfer
  - 八、触摸事件
    - 1、触摸操作概述
    - 2、Touch 接口
    - 2.1 Touch 接口概述 （单个触摸点）
    - 2.2 Touch 接口的实例属性
    - （1）Touch.identifier 触摸点的ID
    - （2）Touch.screenX，Touch.screenY，Touch.clientX，Touch.clientY，pageX，pageY （相对屏幕、浏览器、文档的坐标）
    - （3）Touch.radiusX，Touch.radiusY，Touch.rotationAngle （触摸椭圆区域半径、角度）
    - （4）Touch.force 触摸压力
    - （5）Touch.target 开始触摸时的元素
    - 3、TouchList 接口
    - 4、TouchEvent 接口
    - 4.1 概述
    - 4.2 实例属性
    - （1）TouchEvent.altKey，TouchEvent.ctrlKey，TouchEvent.shiftKey，TouchEvent.metaKey （是否同时按某些功能键）
    - （2）TouchEvent.changedTouches （触摸点集合，不同触摸事件，含义不同）
    - （3）TouchEvent.touches （仍然活动的触摸点集合）
    - （4）TouchEvent.targetTouches （目标元素内活动的触摸点集合）
    - 5、触摸事件的种类
  - 九、拖拉事件
    - 1、拖拉事件的种类
    - 2、DragEvent 接口
    - 3、DataTransfer 接口概述
    - 4、DataTransfer 的实例属性
    - 4.1 DataTransfer.dropEffect 设置接受拖拉的区域的效果
    - 4.2 DataTransfer.effectAllowed 设置被拖拉的节点允许的效果
    - 4.3 DataTransfer.files 本地文件
    - 例子：接收拖拉文件
    - 4.4 DataTransfer.types 数据格式
    - 4.5 DataTransfer.items
    - 5、DataTransfer 的实例方法
    - 5.1 DataTransfer.setData() 设置拖拉事件所带有的数据
    - 5.2 DataTransfer.getData() 返回指定类型的数据
    - 5.3 DataTransfer.clearData() 清除指定或全部数据
    - 5.4 DataTransfer.setDragImage() 设置拖动过程中的图片
  - 十、其他常见事件
    - 1、资源事件
    - 1.1 beforeunload 事件 （关闭窗口前调用）
    - 1.2 unload 事件 （即将关闭窗口时调用）
    - 1.3 load 事件，error 事件 ，abort事件 （页面或某个资源加载成功/失败/取消时调用）
    - 2、session 历史事件
    - 2.1 pageshow 事件（页面显示，加载页面后执行），pagehide 事件（退出当前页面触发）
    - 2.2 popstate 事件（在浏览器的history对象的当前记录发生显式切换时触发）
    - 2.3 hashchange 事件 （hash发生变化时触发）
    - 3、网页状态事件
    - 3.1 DOMContentLoaded 事件 （DOM内容加载完成后触发）
    - 3.2 readystatechange 事件（Document 对象和 XMLHttpRequest 对象的readyState属性发生变化时触发）
    - 4、窗口事件
    - 4.1 scroll 事件 （文档或文档元素滚动时触发）
    - 4.2 resize 事件（窗口大小变化时触发）
    - 4.3 fullscreenchange 事件（元素进入/退出全屏时触发），fullscreenerror 事件（无法切换全屏时触发）
    - 5、剪贴板事件
    - 6、焦点事件
    - 7、CustomEvent 接口（自定义事件）
  - 十一、GlobalEventHandlers 接口 （全局事件处理接口）
    - 1、GlobalEventHandlers.onabort （中断事件）
    - 2、GlobalEventHandlers.onerror （错误事件）
    - 3、GlobalEventHandlers.onload（加载完成事件）、GlobalEventHandlers.onloadstart（开始加载事件）
    - 4、GlobalEventHandlers.onfocus（获取焦点事件），GlobalEventHandlers.onblur（失去焦点事件）
    - 5、GlobalEventHandlers.onscroll（滚动事件）
    - 6、GlobalEventHandlers.oncontextmenu（右键菜单事件），GlobalEventHandlers.onshow（显示右键菜单时触发）
    - 7、其他的事件属性
  - 文档
- 浏览器模型

- 《JavaScript教程》笔记

heBody
2020-01-12
目录

# 事件

# # 事件

## # 一、EventTarget 接口

事件的本质是程序各个组成部分之间的一种通信方式，也是异步编程的一种实现。DOM 支持大量的事件，本章开始介绍 DOM 的事件编程。

### # 1、概述

DOM 的事件操作（监听和触发），都定义在`EventTarget`接口。所有节点对象都部署了这个接口，其他一些需要事件通信的浏览器内置对象（比如，`XMLHttpRequest`、`AudioNode`、`AudioContext`）也部署了这个接口。

该接口主要提供三个实例方法。

- `addEventListener`：绑定事件的监听函数
- `removeEventListener`：移除事件的监听函数
- `dispatchEvent`：触发事件

### # 2、EventTarget.addEventListener()

`EventTarget.addEventListener()`用于在当前节点或对象上，定义一个特定事件的监听函数。一旦这个事件发生，就会执行监听函数。该方法没有返回值。

    target.addEventListener(type, listener[, useCapture]);
    // 事件名称，监听函数 [，是否在捕获阶段触发]

1
2

该方法接受三个参数。

- `type`：事件名称，大小写敏感。
- `listener`：监听函数。事件发生时，会调用该监听函数。
- `useCapture`：布尔值，表示监听函数是否在捕获阶段（capture）触发（参见后文《事件的传播》部分），默认为`false`（监听函数只在冒泡阶段被触发）。该参数可选。

下面是一个例子。

    function hello() {
      console.log('Hello world');
    }

    var button = document.getElementById('btn');
    button.addEventListener('click', hello, false);

1
2
3
4
5
6

上面代码中，`button`节点的`addEventListener`方法绑定`click`事件的监听函数`hello`，该函数只在冒泡阶段触发。

关于参数，有两个地方需要注意。

首先，第二个参数除了监听函数，还可以是一个具有`handleEvent`方法的对象。

    buttonElement.addEventListener('click', {
      handleEvent: function (event) {
        console.log('click');
      }
    });

1
2
3
4
5

上面代码中，`addEventListener`方法的第二个参数，就是一个具有`handleEvent`方法的对象。

其次，第三个参数除了布尔值`useCapture`，还可以是一个属性配置对象。该对象有以下属性。

> - `capture`：布尔值，表示该事件是否在`捕获阶段`触发监听函数。
> - `once`：布尔值，表示监听函数是否只触发一次，然后就自动移除。
> - `passive`：布尔值，表示监听函数不会调用事件的`preventDefault`方法。如果监听函数调用了，浏览器将忽略这个要求，并在监控台输出一行警告。

如果希望事件监听函数只执行一次，可以打开属性配置对象的`once`属性。

    element.addEventListener('click', function (event) {
      // 只执行一次的代码
    }, {once: true});

1
2
3

`addEventListener`方法可以为针对当前对象的同一个事件，添加多个不同的监听函数。这些函数按照添加顺序触发，即先添加先触发。如果为同一个事件多次添加同一个监听函数，该函数只会执行一次，多余的添加将自动被去除（不必使用`removeEventListener`方法手动去除）。

    function hello() {
      console.log('Hello world');
    }

    document.addEventListener('click', hello, false);
    document.addEventListener('click', hello, false);

1
2
3
4
5
6

执行上面代码，点击文档只会输出一行`Hello world`。

如果希望向监听函数传递参数，可以用匿名函数包装一下监听函数。

    function print(x) {
      console.log(x);
    }

    var el = document.getElementById('div1');
    el.addEventListener('click', function () { print('Hello'); }, false);

1
2
3
4
5
6

上面代码通过匿名函数，向监听函数`print`传递了一个参数。

监听函数内部的`this`，指向当前事件所在的那个对象。

    // HTML 代码如下
    // <p id="para">Hello</p>
    var para = document.getElementById('para');
    para.addEventListener('click', function (e) {
      console.log(this.nodeName); // "P"
    }, false);

1
2
3
4
5
6

上面代码中，监听函数内部的`this`指向事件所在的对象`para`。

### # 3、EventTarget.removeEventListener()

`EventTarget.removeEventListener`方法用来移除`addEventListener`方法添加的事件监听函数。该方法没有返回值。

    div.addEventListener('click', listener, false);
    div.removeEventListener('click', listener, false);

1
2

`removeEventListener`方法的参数，与`addEventListener`方法完全一致。它的第一个参数“事件类型”，大小写敏感。

注意，`removeEventListener`方法移除的监听函数，必须是`addEventListener`方法添加的那个监听函数，而且必须在同一个元素节点，否则无效。

    div.addEventListener('click', function (e) {}, false);
    div.removeEventListener('click', function (e) {}, false);// 移除无效，因为不是同一个监听函数

1
2

上面代码中，`removeEventListener`方法无效，因为监听函数不是同一个匿名函数。

    element.addEventListener('mousedown', handleMouseDown, true);
    element.removeEventListener("mousedown", handleMouseDown, false); // 移除无效，第三个参数不一样

1
2

上面代码中，`removeEventListener`方法也是无效的，因为第三个参数不一样。

### # 4、EventTarget.dispatchEvent()

`EventTarget.dispatchEvent`方法在当前节点上触发指定事件，从而触发监听函数的执行。该方法返回一个布尔值，只要有一个监听函数调用了`Event.preventDefault()`，则返回值为`false`，否则为`true`。

    target.dispatchEvent(event)

1

`dispatchEvent`方法的参数是一个`Event`对象的实例（详见《Event 对象》章节）。

    para.addEventListener('click', hello, false);
    var event = new Event('click');
    para.dispatchEvent(event);

1
2
3

上面代码在当前节点触发了`click`事件。

如果`dispatchEvent`方法的参数为空，或者不是一个有效的事件对象，将报错。

下面代码根据`dispatchEvent`方法的返回值，判断事件是否被取消了。

    var canceled = !cb.dispatchEvent(event);
    if (canceled) {
      console.log('事件取消');
    } else {
      console.log('事件未取消');
    }

1
2
3
4
5
6

## # 二、事件模型

### # 1、监听函数

浏览器的事件模型，就是通过监听函数（listener）对事件做出反应。事件发生后，浏览器监听到了这个事件，就会执行对应的监听函数。这是事件驱动编程模式（event-driven）的主要编程方式。

JavaScript 有三种方法，可以为事件绑定监听函数。

#### # 1.2 HTML 的 on- 属性

HTML 语言允许在元素的属性中，直接定义某些事件的监听代码。

    <body onload="doSomething()"> <!-- 加圆括号-->
    <div onclick="console.log('触发事件')">

1
2

上面代码为`body`节点的`load`事件、`div`节点的`click`事件，指定了监听代码。一旦事件发生，就会执行这段代码。

元素的事件监听属性，都是`on`加上事件名，比如`onload`就是`on + load`，表示`load`事件的监听代码。

注意，这些属性的值是将会执行的代码，而不是一个函数。

    <!-- 正确 -->
    <body onload="doSomething()">

    <!-- 错误 -->
    <body onload="doSomething">

1
2
3
4
5

一旦指定的事件发生，`on-`属性的值是原样传入 JavaScript 引擎执行。因此如果要执行函数，不要忘记加上一对圆括号。

使用这个方法指定的监听代码，只会在冒泡阶段触发。

    <div onClick="console.log(2)">
      <button onClick="console.log(1)">点击</button>
    </div>

1
2
3

上面代码中，`<button>`是`<div>`的子元素。`<button>`的`click`事件，也会触发`<div>`的`click`事件。由于`on-`属性的监听代码，只在冒泡阶段触发，所以点击结果是先输出`1`，再输出`2`，即事件从子元素开始冒泡到父元素。

直接设置`on-`属性，与通过元素节点的`setAttribute`方法设置`on-`属性，效果是一样的。

    el.setAttribute('onclick', 'doSomething()');
    // 等同于
    // <Element onclick="doSomething()">

1
2
3

#### # 1.2 元素节点的事件属性 (例：el.onclick)

元素节点对象的事件属性，同样可以指定监听函数。

    window.onload = doSomething; // 注意这里是函数名，不加圆括号

    div.onclick = function (event) { // onclick是元素的一个属性，它指向一个函数，在触发点击时执行此函数
      console.log('触发事件');
    };

1
2
3
4
5

使用这个方法指定的监听函数，也是只会在冒泡阶段触发。

注意，这种方法与 HTML 的`on-`属性的差异是，它的值是函数名（`doSomething`），而不像后者，必须给出完整的监听代码（`doSomething()`）。

#### # 1.3 EventTarget.addEventListener()

所有 DOM 节点实例都有`addEventListener`方法，用来为该节点定义事件的监听函数。

    window.addEventListener('load', doSomething, false); // 注意这里是函数名，不加圆括号

1

`addEventListener`方法的详细介绍，参见`EventTarget`章节。

#### # 1.4 小结

上面三种方法，

第一种“HTML 的 on- 属性”，违反了 HTML 与 JavaScript 代码相分离的原则，将两者写在一起，不利于代码分工，因此不推荐使用。

第二种“元素节点的事件属性”的缺点在于，同一个事件只能定义一个监听函数，也就是说，如果定义两次`onclick`属性，后一次定义会覆盖前一次。因此，也不推荐使用。

第三种`EventTarget.addEventListener`是推荐的指定监听函数的方法。它有如下优点：

- 同一个事件可以添加多个监听函数。
- 能够指定在哪个阶段（捕获阶段还是冒泡阶段）触发监听函数。
- 除了 DOM 节点，其他对象（比如`window`、`XMLHttpRequest`等）也有这个接口，它等于是整个 JavaScript 统一的监听函数接口。

### # 2、this 的指向 （指向元素节点）

监听函数内部的`this`指向触发事件的那个元素节点。（三种事件绑定的this全指向元素节点）

    <button id="btn" onclick="console.log(this.id)">点击</button>

1

执行上面代码，点击后会输出`btn`。

其他两种监听函数的写法，`this`的指向也是如此。

    // HTML 代码如下
    // <button id="btn">点击</button>
    var btn = document.getElementById('btn');

    // 写法一
    btn.onclick = function () {
      console.log(this.id);
    };

    // 写法二
    btn.addEventListener(
      'click',
      function (e) {
        console.log(this.id);
      },
      false
    );

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

上面两种写法，点击按钮以后也是输出`btn`。

### # 3、事件的传播（捕获、目标、冒泡）

一个事件发生后，会在子元素和父元素之间传播（propagation）。这种传播分成三个阶段。

- 第一阶段：从`window`对象传导到目标节点（上层传到底层），称为“捕获阶段”（capture phase）。
- 第二阶段：在目标节点上触发，称为“目标阶段”（target phase）。
- 第三阶段：从目标节点传导回`window`对象（从底层传回上层），称为“冒泡阶段”（bubbling phase）。

这种三阶段的传播模型，使得同一个事件会在多个节点上触发。

    <div>
      <p>点击</p>
    </div>

1
2
3

上面代码中，`<div>`节点之中有一个`<p>`节点。

如果对这两个节点，都设置`click`事件的监听函数（每个节点的捕获阶段和冒泡阶段，各设置一个监听函数），共计设置四个监听函数。然后，对`<p>`点击，`click`事件会触发四次。

    var phases = {
      1: 'capture',
      2: 'target',
      3: 'bubble'
    };

    var div = document.querySelector('div');
    var p = document.querySelector('p');

    div.addEventListener('click', callback, true); // true 表示在捕获阶段触发
    p.addEventListener('click', callback, true); // true 表示在捕获阶段触发
    div.addEventListener('click', callback, false);// fasle 表示在冒泡阶段触发
    p.addEventListener('click', callback, false); // fasle 表示在冒泡阶段触发

    function callback(event) {
      var tag = event.currentTarget.tagName; // 当前目标对象的标签名
      var phase = phases[event.eventPhase]; // 触发的阶段
      console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
    }

    // 点击以后的结果
    // Tag: 'DIV'. EventPhase: 'capture'  捕获阶段
    // Tag: 'P'. EventPhase: 'target' 目标阶段
    // Tag: 'P'. EventPhase: 'target' 目标阶段
    // Tag: 'DIV'. EventPhase: 'bubble' 冒泡阶段

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

上面代码表示，`click`事件被触发了四次：`<div>`节点的捕获阶段和冒泡阶段各1次，`<p>`节点的目标阶段触发了2次。

1. 捕获阶段：事件从`<div>`向`<p>`传播时，触发`<div>`的`click`事件；
2. 目标阶段：事件从`<div>`到达`<p>`时，触发`<p>`的`click`事件；
3. 冒泡阶段：事件从`<p>`传回`<div>`时，再次触发`<div>`的`click`事件。

其中，`<p>`节点有两个监听函数（`addEventListener`方法第三个参数的不同，会导致绑定两个监听函数），因此它们都会因为`click`事件触发一次。所以，`<p>`会在`target`阶段有两次输出。

注意，浏览器总是假定`click`事件的目标节点，就是点击位置嵌套最深的那个节点（本例是`<div>`节点里面的`<p>`节点）。所以，`<p>`节点的捕获阶段和冒泡阶段，都会显示为`target`阶段。

事件传播的最上层对象是`window`，接着依次是`document`，`html`（`document.documentElement`）和`body`（`document.body`）。也就是说，上例的事件传播顺序，在捕获阶段依次为`window`、`document`、`html`、`body`、`div`、`p`，在冒泡阶段依次为`p`、`div`、`body`、`html`、`document`、`window`。

    捕获阶段：
    window(浏览器对象)--> document(文档对象) --> html --> body --> div --> p (点击目标，在目标阶段)

    冒泡阶段：
    p (点击目标，在目标阶段)--> div --> body --> html --> document(文档对象) --> window(浏览器对象)

1
2
3
4
5

### # 4、事件的代理

由于事件会在冒泡阶段向上传播到父节点，因此可以把子节点的监听函数定义在父节点上，由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理（delegation）。

    var ul = document.querySelector('ul');

    ul.addEventListener('click', function (event) {
      if (event.target.tagName.toLowerCase() === 'li') { // 浏览器假定click嵌套最深的元素是目标元素
        // some code
      }
    });

1
2
3
4
5
6
7

上面代码中，`click`事件的监听函数定义在`<ul>`节点，但是实际上，它处理的是子节点`<li>`的`click`事件。这样做的好处是，只要定义一个监听函数，就能处理多个子节点的事件，而不用在每个`<li>`节点上定义监听函数。而且以后再添加子节点，监听函数依然有效。

##### # 阻止事件冒泡 event.stopPropagation()

如果希望事件到某个节点为止，不再传播，可以使用事件对象的`stopPropagation`方法。

    // 事件传播到 p 元素后，就不再向下传播了
    p.addEventListener('click', function (event) {
      event.stopPropagation(); // 该方法在事件对象event上
    }, true); // true表示在捕获阶段绑定事件监听函数

    // 事件冒泡到 p 元素后，就不再向上冒泡了
    p.addEventListener('click', function (event) {
      event.stopPropagation();
    }, false); // false表示在冒泡阶段（默认值）绑定事件监听函数

1
2
3
4
5
6
7
8
9

上面代码中，`stopPropagation`方法分别在捕获阶段和冒泡阶段，阻止了事件的传播。

但是，`stopPropagation`方法只会阻止事件的传播，不会阻止该事件触发`<p>`节点的其他`click`事件的监听函数。也就是说，不是彻底取消`click`事件。

    p.addEventListener('click', function (event) {
      event.stopPropagation();
      console.log(1);
    });

    p.addEventListener('click', function(event) {
      // 会触发
      console.log(2);
    });

1
2
3
4
5
6
7
8
9

上面代码中，`p`元素绑定了两个`click`事件的监听函数。`stopPropagation`方法只能阻止这个事件的传播，不能取消这个事件，因此，第二个监听函数会触发。输出结果会先是1，然后是2。

如果想要彻底取消该事件，不再触发后面所有`click`的监听函数，可以使用`stopImmediatePropagation()`方法。

    p.addEventListener('click', function (event) {
      event.stopImmediatePropagation();
      console.log(1);
    });

    p.addEventListener('click', function(event) {
      // 不会被触发
      console.log(2);
    });

1
2
3
4
5
6
7
8
9

上面代码中，`stopImmediatePropagation`方法可以彻底取消这个事件，使得后面绑定的所有`click`监听函数都不再触发。所以，只会输出1，不会输出2。

## # 三、Event 对象

### # 1. 概述

事件发生以后，会产生一个事件对象，作为参数传给监听函数。浏览器原生提供一个`Event`对象，所有的事件都是这个对象的实例，或者说继承了`Event.prototype`对象。

`Event`对象本身就是一个构造函数，可以用来生成新的实例。

    event = new Event(type, options);// 参数一，事件名称； 参数二，事件对象的配置对象

1

`Event`构造函数接受两个参数。第一个参数`type`是字符串，表示事件的名称；第二个参数`options`是一个对象，表示事件对象的配置。该对象主要有下面两个属性。

- `bubbles`：布尔值，可选，默认为`false`，表示事件对象是否冒泡。
- `cancelable`：布尔值，可选，默认为`false`，表示事件是否可以被取消，即能否用`Event.preventDefault()`取消这个事件。一旦事件被取消，就好像从来没有发生过，不会触发浏览器对该事件的默认行为。

    var ev = new Event( // 创建一个新的事件实例
      'look', // 事件名称
      { // 事件配置
        'bubbles': true, // 事件是否冒泡
        'cancelable': false // 事件是否可以被取消
      }
    );
    document.dispatchEvent(ev); // 触发ev实例，该实例是look事件

1
2
3
4
5
6
7
8

上面代码新建一个`look`事件实例，然后使用`dispatchEvent`方法触发该事件。

注意，如果不是显式指定`bubbles`属性为`true`，生成的事件就只能在“捕获阶段”触发监听函数。

    // HTML 代码为
    // <div><p>Hello</p></div>
    var div = document.querySelector('div');
    var p = document.querySelector('p');

    function callback(event) {
      var tag = event.currentTarget.tagName;
      console.log('Tag: ' + tag); // 没有任何输出
    }

    div.addEventListener('click', callback, false); // 该事件是在冒泡阶段监听函数

    var click = new Event('click');
    p.dispatchEvent(click); // 给p发出一个click事件，该事件默认不会冒泡，因此不会触发父元素div的click事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面代码中，`p`元素发出一个`click`事件，该事件默认不会冒泡。`div.addEventListener`方法指定在冒泡阶段监听，因此监听函数不会触发。如果写成`div.addEventListener('click', callback, true)`，那么在“捕获阶段”可以监听到这个事件。

另一方面，如果这个事件在`div`元素上触发。

    div.dispatchEvent(click);

1

那么，不管`div`元素是在冒泡阶段监听，还是在捕获阶段监听，都会触发监听函数。因为这时`div`元素是事件的目标，不存在是否冒泡的问题，`div`元素总是会接收到事件，因此导致监听函数生效。

### # 2. 实例属性

#### # 2.1 Event.bubbles 是否会冒泡，只读，Event.eventPhase 返回整数，表示事件所处阶段，只读

`Event.bubbles`属性返回一个布尔值，表示当前事件是否会冒泡。该属性为只读属性，一般用来了解 Event 实例是否可以冒泡。前面说过，除非显式声明，`Event`构造函数生成的事件，默认是不冒泡的。

`Event.eventPhase`属性返回一个整数常量，表示事件目前所处的阶段。该属性只读。

    var phase = event.eventPhase;

1

`Event.eventPhase`的返回值有四种可能。

- 0，事件目前没有发生。
- 1，事件目前处于捕获阶段，即处于从祖先节点向目标节点的传播过程中。
- 2，事件到达目标节点，即`Event.target`属性指向的那个节点。
- 3，事件处于冒泡阶段，即处于从目标节点向祖先节点的反向传播过程中。

#### # 2.2 Event.cancelable 是否可取消默认行为，Event.cancelBubble是否阻止冒泡，event.defaultPrevented 是否调用过取消默认行为方法

`Event.cancelable`属性返回一个布尔值，表示事件是否可以取消。该属性为只读属性，一般用来了解 Event 实例的特性。

大多数浏览器的原生事件是可以取消的。比如，取消`click`事件，点击链接将无效。但是除非显式声明，`Event`构造函数生成的事件，默认是不可以取消的。

    var evt = new Event('foo');
    evt.cancelable  // false 默认不可取消

1
2

当`Event.cancelable`属性为`true`时，调用`Event.preventDefault()`就可以取消这个事件，阻止浏览器对该事件的默认行为。

如果事件不能取消，调用`Event.preventDefault()`会没有任何效果。所以使用这个方法之前，最好用`Event.cancelable`属性判断一下是否可以取消。

    function preventEvent(event) {
      if (event.cancelable) {
        event.preventDefault();
      } else {
        console.warn('This event couldn\'t be canceled.');
        console.dir(event);
      }
    }

1
2
3
4
5
6
7
8

`Event.cancelBubble`属性是一个布尔值，如果设为`true`，相当于执行`Event.stopPropagation()`，可以阻止事件的传播。

`Event.defaultPrevented`属性返回一个布尔值，表示该事件是否调用过`Event.preventDefault`方法。该属性只读。

    if (event.defaultPrevented) {
      console.log('该事件已经取消了');
    }

1
2
3

#### # 2.3 Event.currentTarget当前正在通过的节点，Event.target目标节点

事件发生以后，会经过捕获和冒泡两个阶段，依次通过多个 DOM 节点。因此，任意时点都有两个与事件相关的节点，一个是事件的原始触发节点（`Event.target`），另一个是事件当前正在通过的节点（`Event.currentTarget`）。前者通常是后者的后代节点。

`Event.currentTarget`属性返回事件当前所在的节点，即事件当前正在通过的节点，也就是当前正在执行的监听函数所在的那个节点。随着事件的传播，这个属性的值会变。

`Event.target`属性返回原始触发事件的那个节点，即事件最初发生的节点。这个属性不会随着事件的传播而改变。

事件传播过程中，不同节点的监听函数内部的`Event.target`与`Event.currentTarget`属性的值是不一样的。

    // HTML 代码为
    // <p id="para">Hello <em>World</em></p>
    function hide(e) {
      // 不管点击 Hello 或 World，总是返回 true
      console.log(this === e.currentTarget);

      // 点击 Hello，返回 true
      // 点击 World，返回 false
      console.log(this === e.target);
    }

    document.getElementById('para').addEventListener('click', hide, false);

1
2
3
4
5
6
7
8
9
10
11
12

上面代码中，`<em>`是`<p>`的子节点，点击`<em>`或者点击`<p>`，都会导致监听函数执行。这时，`e.target`总是指向原始点击位置的那个节点，而`e.currentTarget`指向事件传播过程中正在经过的那个节点。由于监听函数只有事件经过时才会触发，所以`e.currentTarget`总是等同于监听函数内部的`this`。

#### # 2.4 Event.type 事件类型（如：'click'）

`Event.type`属性返回一个字符串，表示事件类型。事件的类型是在生成事件的时候指定的。该属性只读。

    var evt = new Event('foo');
    evt.type // "foo"

1
2

#### # 2.5 Event.timeStamp 相对于打开网页后的毫秒时间戳

`Event.timeStamp`属性返回一个毫秒时间戳，表示事件发生的时间。它是相对于网页加载成功开始计算的。

    var evt = new Event('foo');
    evt.timeStamp // 3683.6999999995896

1
2

它的返回值有可能是整数，也有可能是小数（高精度时间戳），取决于浏览器的设置。

下面是一个计算鼠标移动速度的例子，显示每秒移动的像素数量。

###### # 例子：计算鼠标移动速度

    var previousX;
    var previousY;
    var previousT;

    window.addEventListener('mousemove', function(event) {
      if (
        previousX !== undefined &&
        previousY !== undefined &&
        previousT !== undefined
      ) {
        var deltaX = event.screenX - previousX;
        var deltaY = event.screenY - previousY;
        var deltaD = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));

        var deltaT = event.timeStamp - previousT;
        console.log(deltaD / deltaT * 1000);
      }

      previousX = event.screenX;
      previousY = event.screenY;
      previousT = event.timeStamp;
    });

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

#### # 2.6 Event.isTrusted 是否由真实用户行为产生的事件

`Event.isTrusted`属性返回一个布尔值，表示该事件是否由真实的用户行为产生。比如，用户点击链接会产生一个`click`事件，该事件是用户产生的；`Event`构造函数生成的事件，则是脚本产生的。

    var evt = new Event('foo');
    evt.isTrusted // false

1
2

上面代码中，`evt`对象是脚本产生的，所以`isTrusted`属性返回`false`。

#### # 2.7 Event.detail 事件的细节（单击or双击等）

`Event.detail`属性只有浏览器的 UI （用户界面）事件才具有。该属性返回一个数值，表示事件的某种信息。具体含义与事件类型相关。比如，对于`click`和`dblclick`事件，`Event.detail`是鼠标按下的次数（`1`表示单击，`2`表示双击，`3`表示三击）；对于鼠标滚轮事件，`Event.detail`是滚轮正向滚动的距离，负值就是负向滚动的距离，返回值总是3的倍数。

    // HTML 代码如下
    // <p>Hello</p>
    function giveDetails(e) {
      console.log(e.detail); // 快速点击的次数
    }

    document.querySelector('p').onclick = giveDetails;

1
2
3
4
5
6
7

### # 3、实例方法

#### # 3.1 Event.preventDefault() 取消浏览器对当前事件的默认行为

`Event.preventDefault`方法取消浏览器对当前事件的默认行为。比如点击链接后，浏览器默认会跳转到另一个页面，使用这个方法以后，就不会跳转了；再比如，按一下空格键，页面向下滚动一段距离，使用这个方法以后也不会滚动了。该方法生效的前提是，事件对象的`cancelable`属性为`true`，如果为`false`，调用该方法没有任何效果。

注意，该方法只是取消事件对当前元素的默认影响，不会阻止事件的传播。如果要阻止传播，可以使用`stopPropagation()`或`stopImmediatePropagation()`方法。

    // HTML 代码为
    // <input type="checkbox" id="my-checkbox" />
    var cb = document.getElementById('my-checkbox');

    cb.addEventListener(
      'click',
      function (e){ e.preventDefault(); },
      false
    );

1
2
3
4
5
6
7
8
9

上面代码中，浏览器的默认行为是单击会选中单选框，取消这个行为，就导致无法选中单选框。

利用这个方法，可以为文本输入框设置校验条件。如果用户的输入不符合条件，就无法将字符输入文本框。

###### # 例子：只能输入字母的输入框

    // HTML 代码为
    // <input type="text" id="my-input" />
    var input = document.getElementById('my-input');
    input.addEventListener('keypress', checkName, false);

    function checkName(e) {
      if (e.charCode < 97 || e.charCode > 122) {
        e.preventDefault();
      }
    }

1
2
3
4
5
6
7
8
9
10

上面代码为文本框的`keypress`事件设定监听函数后，将只能输入小写字母，否则输入事件的默认行为（写入文本框）将被取消，导致不能向文本框输入内容。

#### # 3.2 Event.stopPropagation() 阻止事件传播

`stopPropagation`方法阻止事件在 DOM 中继续传播，防止再触发定义在别的节点上的监听函数，但是不包括在当前节点上其他的事件监听函数。

    function stopEvent(e) {
      e.stopPropagation(); // 阻止事件冒泡
    }

    el.addEventListener('click', stopEvent, false);

1
2
3
4
5

上面代码中，`click`事件将不会进一步冒泡到`el`节点的父节点。

#### # 3.3 Event.stopImmediatePropagation() 阻止同一个事件的其他监听函数被调用

`Event.stopImmediatePropagation`方法阻止同一个事件的其他监听函数被调用，不管监听函数定义在当前节点还是其他节点。也就是说，该方法阻止事件的传播，比`Event.stopPropagation()`更彻底。

如果同一个节点对于同一个事件指定了多个监听函数，这些函数会根据添加的顺序依次调用。只要其中有一个监听函数调用了`Event.stopImmediatePropagation`方法，其他的监听函数就不会再执行了。

    function l1(e){
      e.stopImmediatePropagation();
    }

    function l2(e){ // 不会被调用
      console.log('hello world');
    }

    el.addEventListener('click', l1, false);
    el.addEventListener('click', l2, false);

1
2
3
4
5
6
7
8
9
10

上面代码在`el`节点上，为`click`事件添加了两个监听函数`l1`和`l2`。由于`l1`调用了`event.stopImmediatePropagation`方法，所以`l2`不会被调用。

#### # 3.4 Event.composedPath() 数组，目标和冒泡的节点

`Event.composedPath()`返回一个数组，成员是事件的最底层节点和依次冒泡经过的所有上层节点。

    // HTML 代码如下
    // <div>
    //   <p>Hello</p>
    // </div>
    var div = document.querySelector('div');
    var p = document.querySelector('p');

    div.addEventListener('click', function (e) {
      console.log(e.composedPath());
    }, false);
    // [p, div, body, html, document, Window]

1
2
3
4
5
6
7
8
9
10
11

上面代码中，`click`事件的最底层节点是`p`，向上依次是`div`、`body`、`html`、`document`、`Window`。

## # 四、鼠标事件

### # 1、鼠标事件的种类

鼠标事件指与鼠标相关的事件，继承了`MouseEvent`接口。具体的事件主要有以下一些。

- `click`：按下鼠标（通常是按下主按钮）时触发。【单击】
- `dblclick`：在同一个元素上双击鼠标时触发。【双击】
- `mousedown`：按下鼠标键时触发。【按下】
- `mouseup`：释放按下的鼠标键时触发。【抬起】
- `mousemove`：当鼠标在一个节点内部移动时触发。当鼠标持续移动时，该事件会连续触发。为了避免性能问题，建议对该事件的监听函数做一些限定，比如限定一段时间内只能运行一次。【经过（多次）】
- `mouseenter`：鼠标进入一个节点时触发，进入子节点不会触发这个事件（详见后文）。【进入（单次）】
- `mouseover`：鼠标进入一个节点时触发，进入子节点会再一次触发这个事件（详见后文）。【进入+子节点】
- `mouseleave`：鼠标离开一个节点时触发，离开父节点不会触发这个事件（详见后文）。【离开（单次）】
- `mouseout`：鼠标离开一个节点时触发，离开父节点会触发这个事件（详见后文）。【离开+子节点】
- `contextmenu`：按下鼠标右键时（上下文菜单出现前）触发，或者按下“上下文菜单键”时触发。【右键】
- `wheel`：滚动鼠标的滚轮时触发，该事件继承的是`WheelEvent`接口。【滚轮】

`click`事件指的是，用户在同一个位置先完成`mousedown`动作，再完成`mouseup`动作。因此，触发顺序是，`mousedown`首先触发，`mouseup`接着触发，`click`最后触发。

`dblclick`事件则会在`mousedown`、`mouseup`、`click`之后触发。

`mouseover`事件和`mouseenter`事件，都是鼠标进入一个节点时触发。两者的区别是，`mouseenter`事件只触发一次，而只要鼠标在节点内部移动，`mouseover`事件会在子节点上触发多次。

    /* HTML 代码如下
     <ul>
       <li>item 1</li>
       <li>item 2</li>
      <li>item 3</li>
     </ul>
    */

    var ul = document.querySelector('ul');

    // 进入 ul 节点以后，mouseenter 事件只会触发一次
    // 以后只要鼠标在节点内移动，都不会再触发这个事件
    // event.target 是 ul 节点
    ul.addEventListener('mouseenter', function (event) {
      event.target.style.color = 'purple';
      setTimeout(function () {
        event.target.style.color = '';
      }, 500);
    }, false);

    // 进入 ul 节点以后，只要在子节点上移动，mouseover 事件会触发多次
    // event.target 是 li 节点
    ul.addEventListener('mouseover', function (event) {
      event.target.style.color = 'orange';
      setTimeout(function () {
        event.target.style.color = '';
      }, 500);
    }, false);

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

上面代码中，在父节点内部进入子节点，不会触发`mouseenter`事件，但是会触发`mouseover`事件。

`mouseout`事件和`mouseleave`事件，都是鼠标离开一个节点时触发。两者的区别是，在父元素内部离开一个子元素时，`mouseleave`事件不会触发，而`mouseout`事件会触发。

    /* HTML 代码如下
     <ul>
       <li>item 1</li>
       <li>item 2</li>
      <li>item 3</li>
     </ul>
    */

    var ul = document.querySelector('ul');

    // 先进入 ul 节点，然后在节点内部移动，不会触发 mouseleave 事件
    // 只有离开 ul 节点时，触发一次 mouseleave
    // event.target 是 ul 节点
    ul.addEventListener('mouseleave', function (event) {
      event.target.style.color = 'purple';
      setTimeout(function () {
        event.target.style.color = '';
      }, 500);
    }, false);

    // 先进入 ul 节点，然后在节点内部移动，mouseout 事件会触发多次
    // event.target 是 li 节点
    ul.addEventListener('mouseout', function (event) {
      event.target.style.color = 'orange';
      setTimeout(function () {
        event.target.style.color = '';
      }, 500);
    }, false);

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

上面代码中，在父节点内部离开子节点，不会触发`mouseleave`事件，但是会触发`mouseout`事件。

### # 2、MouseEvent 接口概述

`MouseEvent`接口代表所有鼠标事件所产生的对象都是`MouseEvent`实例。此外，滚轮事件和拖拉事件也是`MouseEvent`实例。

`MouseEvent`接口继承了`Event`接口，所以拥有`Event`的所有属性和方法。它还有自己的属性和方法。

浏览器原生提供一个`MouseEvent`构造函数，用于新建一个`MouseEvent`实例。

    var event = new MouseEvent(type, options);// 参数一，事件名称字符串；参数二，事件配置对象

1

`MouseEvent`构造函数接受两个参数。第一个参数是字符串，表示事件名称；第二个参数是一个事件配置对象，该参数可选。除了`Event`接口的实例配置属性，该对象可以配置以下属性，所有属性都是可选的。

- `screenX`：数值，鼠标相对于屏幕的水平位置（单位像素），默认值为0，设置该属性不会移动鼠标。
- `screenY`：数值，鼠标相对于屏幕的垂直位置（单位像素），其他与`screenX`相同。
- `clientX`：数值，鼠标相对于程序窗口的水平位置（单位像素），默认值为0，设置该属性不会移动鼠标。
- `clientY`：数值，鼠标相对于程序窗口的垂直位置（单位像素），其他与`clientX`相同。
- `ctrlKey`：布尔值，是否同时按下了 Ctrl 键，默认值为`false`。
- `shiftKey`：布尔值，是否同时按下了 Shift 键，默认值为`false`。
- `altKey`：布尔值，是否同时按下 Alt 键，默认值为`false`。
- `metaKey`：布尔值，是否同时按下 Meta 键（win键），默认值为`false`。
- `button`：数值，表示按下了哪一个鼠标按键，默认值为`0`，表示按下主键（通常是鼠标的左键）或者当前事件没有定义这个属性；`1`表示按下辅助键（通常是鼠标的中间键），`2`表示按下次要键（通常是鼠标的右键）。
- `buttons`：数值，表示按下了鼠标的哪些键，是一个三个比特位的二进制值，默认为`0`（没有按下任何键）。`1`（二进制`001`）表示按下主键（通常是左键），`2`（二进制`010`）表示按下次要键（通常是右键），`4`（二进制`100`）表示按下辅助键（通常是中间键）。因此，如果返回`3`（二进制`011`）就表示同时按下了左键和右键。
- `relatedTarget`：节点对象，表示事件的相关节点，默认为`null`。`mouseenter`和`mouseover`事件时，表示鼠标刚刚离开的那个元素节点；`mouseout`和`mouseleave`事件时，表示鼠标正在进入的那个元素节点。

下面是一个例子。

    var event = new MouseEvent('click2', {
        'bubbles': true,
        'cancelable': true
    });
    var cb = document.getElementById('checkbox');
    cb.addEventListener('click2',function(){ // 绑定事件监听函数
        console.log(22) // 被执行
    })
    cb.dispatchEvent(event);// 触发事件

1
2
3
4
5
6
7
8
9

上面代码生成一个鼠标点击事件，并触发该事件。

### # 3、MouseEvent 接口的实例属性

#### # 3.1 MouseEvent.altKey，MouseEvent.ctrlKey，MouseEvent.metaKey，MouseEvent.shiftKey

`MouseEvent.altKey`、`MouseEvent.ctrlKey`、`MouseEvent.metaKey`、`MouseEvent.shiftKey`这四个属性都返回一个布尔值，表示事件发生时，是否按下对应的键。它们都是只读属性。

- `altKey`属性：Alt 键
- `ctrlKey`属性：Ctrl 键
- `metaKey`属性：Meta 键（Mac 键盘是一个四瓣的小花，Windows 键盘是 Windows 键）
- `shiftKey`属性：Shift 键

    // HTML 代码如下
    // <body onclick="showKey(event)">
    function showKey(e) {
      console.log('ALT key pressed: ' + e.altKey);
      console.log('CTRL key pressed: ' + e.ctrlKey);
      console.log('META key pressed: ' + e.metaKey);
      console.log('SHIFT key pressed: ' + e.shiftKey);
    }

1
2
3
4
5
6
7
8

上面代码中，点击网页会输出是否同时按下对应的键。

#### # 3.2 MouseEvent.button 鼠标的哪个键，MouseEvent.buttons同时按哪些键

`MouseEvent.button`属性返回一个数值，表示事件发生时按下了鼠标的哪个键。该属性只读。

- 0：按下主键（通常是左键），或者该事件没有初始化这个属性（比如`mousemove`事件）。
- 1：按下辅助键（通常是中键或者滚轮键）。
- 2：按下次键（通常是右键）。

    // HTML 代码为
    // <button onmouseup="whichButton(event)">点击</button>
    var whichButton = function (e) {
      switch (e.button) {
        case 0:
          console.log('Left button clicked.');
          break;
        case 1:
          console.log('Middle button clicked.');
          break;
        case 2:
          console.log('Right button clicked.');
          break;
        default:
          console.log('Unexpected code: ' + e.button);
      }
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

`MouseEvent.buttons`属性返回一个三个比特位的值，表示同时按下了哪些键。它用来处理同时按下多个鼠标键的情况。该属性只读。

- 1：二进制为`001`（十进制的1），表示按下左键。
- 2：二进制为`010`（十进制的2），表示按下右键。
- 4：二进制为`100`（十进制的4），表示按下中键或滚轮键。

同时按下多个键的时候，每个按下的键对应的比特位都会有值。比如，同时按下左键和右键，会返回3（二进制为011）。

    document.body.addEventListener('mousemove',function(e){ // 注意，用click时一直都是0
        console.log(e.buttons)
    })
    // 未按下任何键时是 0
    // 按下左键 1  （001）
    // 按下右键 2  （010）
    // 按下中键 4  （100）

    // 按下左键和右键 3  （011）
    // 按下左键和中键 5  （101）
    // 按下右键和中键 6  （110）
    // 按下左、中、右键 7  （111）

1
2
3
4
5
6
7
8
9
10
11
12

#### # 3.3 MouseEvent.clientX 相对浏览器X坐标，MouseEvent.clientY 相对浏览器Y坐标

`MouseEvent.clientX`属性返回鼠标位置相对于浏览器窗口左上角的水平坐标（单位像素），`MouseEvent.clientY`属性返回垂直坐标。这两个属性都是只读属性。

    // HTML 代码为
    // <body onmousedown="showCoords(event)">
    function showCoords(evt){
      console.log(
        'clientX value: ' + evt.clientX + '\n' +
        'clientY value: ' + evt.clientY + '\n'
      );
    }

1
2
3
4
5
6
7
8

这两个属性还分别有一个别名`MouseEvent.x`和`MouseEvent.y`。

#### # 3.4 MouseEvent.movementX 上一个鼠标经过事件的X距离，MouseEvent.movementY 上一个鼠标经过事件的Y距离

`MouseEvent.movementX`属性返回当前位置与上一个`mousemove`事件之间的水平距离（单位像素）。数值上，它等于下面的计算公式。

    currentEvent.movementX = currentEvent.screenX - previousEvent.screenX

1

`MouseEvent.movementY`属性返回当前位置与上一个`mousemove`事件之间的垂直距离（单位像素）。数值上，它等于下面的计算公式。

    currentEvent.movementY = currentEvent.screenY - previousEvent.screenY。

1

这两个属性都是只读属性。

#### # 3.5 MouseEvent.screenX 相对屏幕X坐标，MouseEvent.screenY 相对屏幕Y坐标

`MouseEvent.screenX`属性返回鼠标位置相对于屏幕左上角的水平坐标（单位像素），`MouseEvent.screenY`属性返回垂直坐标。这两个属性都是只读属性。

    // HTML 代码如下
    // <body onmousedown="showCoords(event)">
    function showCoords(evt) {
      console.log(
        'screenX value: ' + evt.screenX + '\n',
        'screenY value: ' + evt.screenY + '\n'
      );
    }

1
2
3
4
5
6
7
8

#### # 3.6 MouseEvent.offsetX 偏移量X，MouseEvent.offsetY 偏移量Y

`MouseEvent.offsetX`属性返回鼠标位置与目标节点左侧的`padding`边缘的水平距离（单位像素），`MouseEvent.offsetY`属性返回与目标节点上方的`padding`边缘的垂直距离。这两个属性都是只读属性。

    /* HTML 代码如下
      <style>
        p {
          width: 100px;
          height: 100px;
          padding: 100px;
        }
      </style>
      <p>Hello</p>
    */
    var p = document.querySelector('p');
    p.addEventListener(
      'click',
      function (e) {
        console.log(e.offsetX); // 包含padding
        console.log(e.offsetY);
      },
      false
    );

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

上面代码中，鼠标如果在`p`元素的中心位置点击，会返回`150 150`。因此中心位置距离左侧和上方的`padding`边缘，等于`padding`的宽度（100像素）加上元素内容区域一半的宽度（50像素）。

#### # 3.7 MouseEvent.pageX 文档X坐标，MouseEvent.pageY 文档Y坐标

`MouseEvent.pageX`属性返回鼠标位置与文档左侧边缘的距离（单位像素），`MouseEvent.pageY`属性返回与文档上侧边缘的距离（单位像素）。它们的返回值都包括文档不可见的部分。这两个属性都是只读。

    /* HTML 代码如下
      <style>
        body {
          height: 2000px;
        }
      </style>
    */
    document.body.addEventListener(
      'click',
      function (e) {
        console.log(e.pageX);
        console.log(e.pageY);
      },
      false
    );

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上面代码中，页面高度为2000像素，会产生垂直滚动条。滚动到页面底部，点击鼠标输出的`pageY`值会接近2000。

#### # 3.8 MouseEvent.relatedTarget 事件的相关节点

`MouseEvent.relatedTarget`属性返回事件的相关节点。对于那些没有相关节点的事件，该属性返回`null`。该属性只读。

下表列出不同事件的`target`属性值和`relatedTarget`属性值义。

事件名称 | target 属性 | relatedTarget 属性
--- | --- | ---
focusin | 接受焦点的节点 | 丧失焦点的节点
focusout | 丧失焦点的节点 | 接受焦点的节点
mouseenter | 将要进入的节点 | 将要离开的节点
mouseleave | 将要离开的节点 | 将要进入的节点
mouseout | 将要离开的节点 | 将要进入的节点
mouseover | 将要进入的节点 | 将要离开的节点
dragenter | 将要进入的节点 | 将要离开的节点
dragexit | 将要离开的节点 | 将要进入的节点

下面是一个例子。

    /*
      HTML 代码如下
      <div id="outer" style="height:50px;width:50px;border-width:1px solid black;">
        <div id="inner" style="height:25px;width:25px;border:1px solid black;"></div>
      </div>
    */

    var inner = document.getElementById('inner');
    inner.addEventListener('mouseover', function (event) {
      console.log('进入' + event.target.id + ' 离开' + event.relatedTarget.id);
    }, false);
    inner.addEventListener('mouseenter', function (event) {
      console.log('进入' + event.target.id + ' 离开' + event.relatedTarget.id);
    });
    inner.addEventListener('mouseout', function () {
      console.log('离开' + event.target.id + ' 进入' + event.relatedTarget.id);
    });
    inner.addEventListener("mouseleave", function (){
      console.log('离开' + event.target.id + ' 进入' + event.relatedTarget.id);
    });

    // 鼠标从 outer 进入inner，输出
    // 进入inner 离开outer
    // 进入inner 离开outer

    // 鼠标从 inner进入 outer，输出
    // 离开inner 进入outer
    // 离开inner 进入outer

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

### # 4、MouseEvent 接口的实例方法

#### # 4.1 MouseEvent.getModifierState() 是否按下指定功能键

`MouseEvent.getModifierState`方法返回一个布尔值，表示有没有按下特定的功能键。它的参数是一个表示功能键
(opens new window)的字符串。

    document.addEventListener('click', function (e) {
      console.log(e.getModifierState('CapsLock'));
    }, false);

1
2
3

上面的代码可以了解用户是否按下了大写键。

### # 5、WheelEvent 接口 （滚轮）

#### # 5.1 概述

WheelEvent 接口继承了 MouseEvent 实例，代表鼠标滚轮事件的实例对象。目前，鼠标滚轮相关的事件只有一个`wheel`事件，用户滚动鼠标的滚轮，就生成这个事件的实例。

浏览器原生提供`WheelEvent()`构造函数，用来生成`WheelEvent`实例。

    var wheelEvent = new WheelEvent(type, options);

1

`WheelEvent()`构造函数可以接受两个参数，第一个是字符串，表示事件类型，对于滚轮事件来说，这个值目前只能是`wheel`。第二个参数是事件的配置对象。该对象的属性除了`Event`、`UIEvent`的配置属性以外，还可以接受以下几个属性，所有属性都是可选的。

- `deltaX`：数值，表示滚轮的水平滚动量，默认值是 0.0。
- `deltaY`：数值，表示滚轮的垂直滚动量，默认值是 0.0。
- `deltaZ`：数值，表示滚轮的 Z 轴滚动量，默认值是 0.0。
- `deltaMode`：数值，表示相关的滚动事件的单位，适用于上面三个属性。`0`表示滚动单位为像素，`1`表示单位为行，`2`表示单位为页，默认为`0`。

#### # 5.2 实例属性

`WheelEvent`事件实例除了具有`Event`和`MouseEvent`的实例属性和实例方法，还有一些自己的实例属性，但是没有自己的实例方法。

下面的属性都是只读属性。

- `WheelEvent.deltaX`：数值，表示滚轮的水平滚动量。
- `WheelEvent.deltaY`：数值，表示滚轮的垂直滚动量。
- `WheelEvent.deltaZ`：数值，表示滚轮的 Z 轴滚动量。
- `WheelEvent.deltaMode`：数值，表示上面三个属性的单位，`0`是像素，`1`是行，`2`是页。

## # 五、键盘事件

### # 1、键盘事件的种类

键盘事件由用户击打键盘触发，主要有`keydown`、`keypress`、`keyup`三个事件，它们都继承了`KeyboardEvent`接口。

- `keydown`：按下键盘时触发。【按下】
- `keypress`：按下有值的键时触发，即按下 Ctrl、Alt、Shift、Meta 这样无值的键，这个事件不会触发。对于有值的键，按下时先触发`keydown`事件，再触发这个事件。【按下有值的键】
- `keyup`：松开键盘时触发该事件。【松开】

如果用户一直按键不松开，就会连续触发键盘事件，触发的顺序如下。

1. keydown
2. keypress
3. keydown
4. keypress
5. ...（重复以上过程）
6. keyup

### # 2、KeyboardEvent 接口概述

`KeyboardEvent`接口用来描述用户与键盘的互动。这个接口继承了`Event`接口，并且定义了自己的实例属性和实例方法。

浏览器原生提供`KeyboardEvent`构造函数，用来新建键盘事件的实例。

    new KeyboardEvent(type, options) // 参数一，事件类型；参数二，事件配置对象

1

`KeyboardEvent`构造函数接受两个参数。第一个参数是字符串，表示事件类型；第二个参数是一个事件配置对象，该参数可选。除了`Event`接口提供的属性，还可以配置以下字段，它们都是可选。

- `key`：字符串，当前按下的键，默认为空字符串。【键名】
- `code`：字符串，表示当前按下的键的字符串形式，默认为空字符串。【键码】
- `location`：整数，当前按下的键的位置，默认为`0`。
- `ctrlKey`：布尔值，是否按下 Ctrl 键，默认为`false`。
- `shiftKey`：布尔值，是否按下 Shift 键，默认为`false`。
- `altKey`：布尔值，是否按下 Alt 键，默认为`false`。
- `metaKey`：布尔值，是否按下 Meta 键，默认为`false`。
- `repeat`：布尔值，是否重复按键，默认为`false`。

### # 3、KeyboardEvent 的实例属性

#### # 3.1 KeyboardEvent.altKey，KeyboardEvent.ctrlKey，KeyboardEvent.metaKey，KeyboardEvent.shiftKey 【是否按下对应键，布尔值】

以下属性都是只读属性，返回一个布尔值，表示是否按下对应的键。

- `KeyboardEvent.altKey`：是否按下 Alt 键
- `KeyboardEvent.ctrlKey`：是否按下 Ctrl 键
- `KeyboardEvent.metaKey`：是否按下 meta 键（Mac 系统是一个四瓣的小花，Windows 系统是 windows 键）
- `KeyboardEvent.shiftKey`：是否按下 Shift 键

下面是一个示例。

    function showChar(e) {
      console.log('ALT: ' + e.altKey);
      console.log('CTRL: ' + e.ctrlKey);
      console.log('Meta: ' + e.metaKey);
      console.log('Shift: ' + e.shiftKey);
    }

    document.body.addEventListener('keydown', showChar, false);

1
2
3
4
5
6
7
8

#### # 3.2 KeyboardEvent.code 键码

`KeyboardEvent.code`属性返回一个字符串，表示当前按下的键的字符串形式。该属性只读。

下面是一些常用键的字符串形式，其他键请查文档
(opens new window)。

- 数字键0 - 9：返回`digital0` - `digital9`
- 字母键A - z：返回`KeyA` - `KeyZ`
- 功能键F1 - F12：返回 `F1` - `F12`
- 方向键：返回`ArrowDown`、`ArrowUp`、`ArrowLeft`、`ArrowRight`
- Alt 键：返回`AltLeft`或`AltRight`
- Shift 键：返回`ShiftLeft`或`ShiftRight`
- Ctrl 键：返回`ControlLeft`或`ControlRight`

#### # 3.3 KeyboardEvent.key 键名

`KeyboardEvent.key`属性返回一个字符串，表示按下的键名。该属性只读。

如果按下的键代表可打印字符，则返回这个字符，比如数字、字母。

如果按下的键代表不可打印的特殊字符，则返回预定义的键值，比如 Backspace，Tab，Enter，Shift，Control，Alt，CapsLock，Esc，Spacebar，PageUp，PageDown，End，Home，Left，Right，Up，Down，PrintScreen，Insert，Del，Win，F1～F12，NumLock，Scroll 等。

如果同时按下一个控制键和一个符号键，则返回符号键的键名。比如，按下 Ctrl + a，则返回`a`；按下 Shift + a，则返回大写的`A`。

如果无法识别键名，返回字符串`Unidentified`。

#### # 3.4 KeyboardEvent.location 键处于哪个位置，整数

`KeyboardEvent.location`属性返回一个整数，表示按下的键处在键盘的哪一个区域。它可能取以下值。

- 0：处在键盘的主区域，或者无法判断处于哪一个区域。

- 1：处在键盘的左侧，只适用那些有两个位置的键（比如 Ctrl 和 Shift 键）。

- 2：处在键盘的右侧，只适用那些有两个位置的键（比如 Ctrl 和 Shift 键）。

- 3：处在数字小键盘。

#### # 3.5 KeyboardEvent.repeat 是否长按

`KeyboardEvent.repeat`返回一个布尔值，代表该键是否被按着不放，以便判断是否重复这个键，即浏览器会持续触发`keydown`和`keypress`事件，直到用户松开手为止。

### # 4、KeyboardEvent 的实例方法

#### # 4.1 KeyboardEvent.getModifierState() 是否按下指定功能键

`KeyboardEvent.getModifierState()`方法返回一个布尔值，表示是否按下或激活指定的功能键。它的常用参数如下。

- `Alt`：Alt 键
- `CapsLock`：大写锁定键
- `Control`：Ctrl 键
- `Meta`：Meta 键
- `NumLock`：数字键盘开关键
- `Shift`：Shift 键

    if (
      event.getModifierState('Control') +
      event.getModifierState('Alt') +
      event.getModifierState('Meta') > 1
    ) {
      return;
    }

1
2
3
4
5
6
7

上面代码表示，只要`Control`、`Alt`、`Meta`里面，同时按下任意两个或两个以上的键就返回。

## # 六、进度事件

### # 1、进度事件的种类

进度事件用来描述资源加载的进度，主要由 AJAX 请求、`<img>`、`<audio>`、`<video>`、`<style>`、`<link>`等外部资源的加载触发，继承了`ProgressEvent`接口。它主要包含以下几种事件。

- `abort`：外部资源中止加载时（比如用户取消）触发。如果发生错误导致中止，不会触发该事件。【中止加载】
- `error`：由于错误导致外部资源无法加载时触发。【加载错误】
- `load`：外部资源加载成功时触发。【加载成功】
- `loadstart`：外部资源开始加载时触发。【开始加载】
- `loadend`：外部资源停止加载时触发，发生顺序排在`error`、`abort`、`load`等事件的后面。【停止加载】
- `progress`：外部资源加载过程中不断触发。【加载中，不断触发】
- `timeout`：加载超时时触发。【加载超时】

注意，除了资源下载，文件上传也存在这些事件。

下面是一个例子。

    image.addEventListener('load', function (event) { // 加载成功
      image.classList.add('finished');
    });

    image.addEventListener('error', function (event) { // 加载出错
      image.style.display = 'none';
    });

1
2
3
4
5
6
7

上面代码在图片元素加载完成后，为图片元素添加一个`finished`的 Class。如果加载失败，就把图片元素的样式设置为不显示。

有时候，图片加载会在脚本运行之前就完成，尤其是当脚本放置在网页底部的时候，因此有可能`load`和`error`事件的监听函数根本不会执行。所以，比较可靠的方式，是用`complete`属性先判断一下是否加载完成。

    function loaded() {
      // ...
    }

    if (image.complete) { // 是否加载完成
      loaded();
    } else {
      image.addEventListener('load', loaded); // 加载成功事件
    }

1
2
3
4
5
6
7
8
9

由于 DOM 的元素节点没有提供是否加载错误的属性，所以`error`事件的监听函数最好放在`<img>`元素的 HTML 代码中，这样才能保证发生加载错误时百分之百会执行。

    <img src="/wrong/url" onerror="this.style.display='none';" />

1

`loadend`事件的监听函数，可以用来取代`abort`事件、`load`事件、`error`事件的监听函数，因为它总是在这些事件之后发生。

    req.addEventListener('loadend', loadEnd, false);

    function loadEnd(e) {
      console.log('传输结束，成功失败未知');
    }

1
2
3
4
5

`loadend`事件本身不提供关于进度结束的原因，但可以用它来做所有加载结束场景都需要做的一些操作。

另外，`error`事件有一个特殊的性质，就是不会冒泡。所以，子元素的`error`事件，不会触发父元素的`error`事件监听函数。

### # 2、ProgressEvent 接口

#### # 2.1 概述

`ProgressEvent`接口主要用来描述外部资源加载的进度，比如 AJAX 加载、`<img>`、`<video>`、`<style>`、`<link>`等外部资源加载。进度相关的事件都继承了这个接口。这个接口继承了Event接口。

浏览器原生提供了`ProgressEvent()`构造函数，用来生成事件实例。

    new ProgressEvent(type, options) // 参数一，事件类型；参数二，配置对象

1

`ProgressEvent()`构造函数接受两个参数。第一个参数是字符串，表示事件的类型，这个参数是必须的。第二个参数是一个配置对象，表示事件的属性，该参数可选。配置对象除了可以使用`Event`接口的配置属性，还可以使用下面的属性，所有这些属性都是可选的。

- `lengthComputable`：布尔值，表示加载的总量是否可以计算，默认是`false`。
- `loaded`：整数，表示已经加载的量，默认是`0`。
- `total`：整数，表示需要加载的总量，默认是`0`。

#### # 2.2 ProgressEvent的实例属性。

- `ProgressEvent.lengthComputable` 总量是否可以计算
- `ProgressEvent.loaded` 已加载的量
- `ProgressEvent.total` 需要加载的总量

如果`ProgressEvent.lengthComputable`为`false`，`ProgressEvent.total`实际上是没有意义的。

下面是一个例子。

    var p = new ProgressEvent('load', {
      lengthComputable: true,
      loaded: 30,
      total: 100,
    });

    document.body.addEventListener('load', function (e) {
      console.log('已经加载：' + (e.loaded / e.total) * 100 + '%');
    });

    document.body.dispatchEvent(p);
    // 已经加载：30%

1
2
3
4
5
6
7
8
9
10
11
12

上面代码先构造一个`load`事件，抛出后被监听函数捕捉到。

下面是一个实际的例子。

    var xhr = new XMLHttpRequest();

    xhr.addEventListener('progress', updateProgress, false); // 加载中
    xhr.addEventListener('load', transferComplete, false); // 加载成功
    xhr.addEventListener('error', transferFailed, false); // 加载错误
    xhr.addEventListener('abort', transferCanceled, false); // 中止加载

    xhr.open();

    function updateProgress(e) { // 加载中
      if (e.lengthComputable) { // 是否可以计算总量
        var percentComplete = e.loaded / e.total; // 加载进度计算
      } else {
        console.log('不能计算进度');
      }
    }

    function transferComplete(e) { // 加载成功
      console.log('传输结束');
    }

    function transferFailed(evt) { // 加载错误
      console.log('传输过程中发生错误');
    }

    function transferCanceled(evt) { // 中止加载
      console.log('用户取消了传输');
    }

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

上面是下载过程的进度事件，还存在上传过程的进度事件。这时所有监听函数都要放在`XMLHttpRequest.upload`对象上面。

    var xhr = new XMLHttpRequest();

    xhr.upload.addEventListener('progress', updateProgress, false);
    xhr.upload.addEventListener('load', transferComplete, false);
    xhr.upload.addEventListener('error', transferFailed, false);
    xhr.upload.addEventListener('abort', transferCanceled, false);

    xhr.open();

1
2
3
4
5
6
7
8

## # 七、表单事件

### # 1、表单事件的种类

#### # 1.1 input 事件 （值发生变化触发，会连续）

`input`事件当`<input>`、`<select>`、`<textarea>`的值发生变化时触发。对于复选框（`<input type=checkbox>`）或单选框（`<input type=radio>`），用户改变选项时，也会触发这个事件。另外，对于打开`contenteditable`属性的元素，只要值发生变化，也会触发`input`事件。

`input`事件的一个特点，就是会连续触发，比如用户每按下一次按键，就会触发一次`input`事件。

`input`事件对象继承了`InputEvent`接口。

该事件跟`change`事件很像，不同之处在于**`input`事件在元素的值发生变化后立即发生**，而`change`在元素失去焦点时发生，而内容此时可能已经变化多次。也就是说，如果有连续变化，`input`事件会触发多次，而`change`事件只在失去焦点时触发一次。

下面是`<select>`元素的例子。

    /* HTML 代码如下
    <select id="mySelect">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
    */

    function inputHandler(e) {
      console.log(e.target.value)
    }

    var mySelect = document.querySelector('#mySelect');
    mySelect.addEventListener('input', inputHandler);

1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面代码中，改变下拉框选项时，会触发`input`事件，从而执行回调函数`inputHandler`。

#### # 1.2 select 事件 （选中文本时触发）

`select`事件当在`<input>`、`<textarea>`里面选中文本时触发。

    // HTML 代码如下
    // <input id="test" type="text" value="Select me!" />

    var elem = document.getElementById('test');
    elem.addEventListener('select', function (e) {
      console.log(e.type); // "select" 事件类型

      var _target = e.target;
      console.log(_target.value); // 文本框的全部值
      console.log(_target.selectionDirection); // 选择的方向：'forward'正向、'backward'反向

      console.log(_target.selectionStart); // 开始选择的索引
      console.log(_target.selectionEnd); // 结束选择的索引
      // 注意：开始和结束索引是不分选择方向的，开始的索引一直是靠前的

      console.log(_target.value.slice(_target.selectionStart, _target.selectionEnd)) // 选中的那部分字符串
    }, false);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

选中的文本可以通过`event.target`元素的`selectionDirection`、`selectionEnd`、`selectionStart`和`value`属性拿到。

#### # 1.3 change 事件 （值发生变化时触发，单次）

`change`事件当`<input>`、`<select>`、`<textarea>`的值发生变化时触发。它与`input`事件的最大不同，就是不会连续触发，只有当全部修改完成时才会触发，另一方面`input`事件必然伴随`change`事件。具体来说，分成以下几种情况。

- 激活单选框（radio）或复选框（checkbox）时触发。
- 用户提交时触发。比如，从下列列表（select）完成选择，在日期或文件输入框完成选择。
- 当文本框或`<textarea>`元素的值发生改变，并且丧失焦点时触发。

下面是一个例子。

    // HTML 代码如下
    // <select size="1" onchange="changeEventHandler(event);">
    //   <option>chocolate</option>
    //   <option>strawberry</option>
    //   <option>vanilla</option>
    // </select>

    function changeEventHandler(event) {
      console.log(event.target.value);
    }

1
2
3
4
5
6
7
8
9
10

如果比较一下上面`input`事件的例子，你会发现对于`<select>`元素来说，`input`和`change`事件基本是等价的。

#### # 1.4 invalid 事件 （表单提交不满足条件触发）

用户提交表单时，如果表单元素的值不满足校验条件，就会触发`invalid`事件。

    <form>
      <input type="text" required oninvalid="console.log('invalid input')" />
      <button type="submit">提交</button>
    </form>

1
2
3
4

上面代码中，输入框是必填的。如果不填，用户点击按钮提交时，就会触发输入框的`invalid`事件，导致提交被取消。

#### # 1.5 reset 事件（重置），submit 事件（提交）

`reset`事件当表单重置（所有表单成员变回默认值）时触发。

`submit`事件当表单数据向服务器提交时触发。

注意，这两个事件发生在表单对象`<form>`上，而不是发生在表单的成员上，因为提交的是表单，而不是表单成员。

    <form onreset="console.log('触发了重置事件')" onsubmit="console.log('触发了提交事件')" name="input" action="html_form_action.php" method="get">
        <input type="text" name="lname" value="Duck">

        <button type="reset">重置</button>
        <button type="submit">提交</button>
    </form>

1
2
3
4
5
6

### # 2、InputEvent 接口（input事件的实例）

`InputEvent`接口主要用来描述`input`事件的实例。该接口继承了`Event`接口，还定义了一些自己的实例属性和实例方法。

浏览器原生提供`InputEvent()`构造函数，用来生成实例对象。

    new InputEvent(type, options) // 参数一，事件名称；参数二，配置对象

1

`InputEvent`构造函数可以接受两个参数。第一个参数是字符串，表示事件名称，该参数是必需的。第二个参数是一个配置对象，用来设置事件实例的属性，该参数是可选的。配置对象的字段除了`Event`构造函数的配置属性，还可以设置下面的字段，这些字段都是可选的。

- `inputType`：字符串，表示发生变更的类型（详见下文）。
- `data`：字符串，表示插入的字符串。如果没有插入的字符串（比如删除操作），则返回`null`或空字符串。
- `dataTransfer`：返回一个 DataTransfer 对象实例，该属性通常只在输入框接受富文本输入时有效。

`InputEvent`的实例属性主要就是上面三个属性，这三个实例属性都是只读的。

#### # （1）InputEvent.data 变动的那部分内容

`InputEvent.data`属性返回一个字符串，表示变动的内容。

    // HTML 代码如下
    // <input type="text" id="myInput">
    var input = document.getElementById('myInput');
    input.addEventListener('input', myFunction, false);

    function myFunction(e) {
      console.log(e.data);
    }

1
2
3
4
5
6
7
8

上面代码中，如果手动在输入框里面输入`abc`，控制台会先输出`a`，再在下一行输出`b`，再在下一行输出`c`。然后选中`abc`，一次性将它们删除，控制台会输出`null`或一个空字符串。

#### # （2）InputEvent.inputType 变更类型

`InputEvent.inputType`属性返回一个字符串，表示字符串发生变更的类型。

对于常见情况，Chrome 浏览器的返回值如下。完整列表可以参考文档
(opens new window)。

- 手动插入文本：`insertText`
- 粘贴插入文本：`insertFromPaste`
- 向后删除：`deleteContentBackward`
- 向前删除：`deleteContentForward`

#### # （3）InputEvent.dataTransfer

`InputEvent.dataTransfer`属性返回一个 DataTransfer 实例。该属性只在文本框接受粘贴内容（insertFromPaste）或拖拽内容（`insertFromDrop`）时才有效。

## # 八、触摸事件

### # 1、触摸操作概述

浏览器的触摸 API 由三个部分组成。

- Touch：一个触摸点的实例
- TouchList：多个触摸点集合的实例
- TouchEvent：触摸引发的事件实例

`Touch`接口的实例对象用来表示触摸点（一根手指或者一根触摸笔），包括位置、大小、形状、压力、目标元素等属性。有时，触摸动作由多个触摸点（多根手指）组成，多个触摸点的集合由`TouchList`接口的实例对象表示。`TouchEvent`接口的实例对象代表由触摸引发的事件，只有触摸屏才会引发这一类事件。

很多时候，触摸事件和鼠标事件同时触发，即使这个时候并没有用到鼠标。这是为了让那些只定义鼠标事件、没有定义触摸事件的代码，在触摸屏的情况下仍然能用。如果想避免这种情况，可以用`event.preventDefault`方法阻止发出鼠标事件。

    TouchEvent {isTrusted: true, touches: TouchList, targetTouches: TouchList, changedTouches: TouchList, altKey: false, …} // TouchEvent接口 ，继承Event接口属性和方法
        altKey: false
        bubbles: true
        cancelBubble: false
        cancelable: false
        changedTouches: TouchList // TouchList 接口 （所有触摸点集合）
                0: Touch // Touch 接口 （单个触摸点）
                    clientX: 232
                    clientY: 96
                    force: 1 // 触摸压力
                    identifier: 0 // 唯一ID
                    pageX: 232
                    pageY: 96
                    radiusX: 11.5
                    radiusY: 11.5
                    region: null
                    rotationAngle: 0
                    screenX: 476
                    screenY: 266
                    target: html // 触摸目标元素
                    __proto__: Touch
                    length: 1

        	__proto__: TouchList

        composed: true
        ctrlKey: false
        currentTarget: null
        defaultPrevented: false
        detail: 0
        eventPhase: 0
        isTrusted: true
        metaKey: false
        path: (3) [html, document, Window]
        returnValue: true
        shiftKey: false
        sourceCapabilities: InputDeviceCapabilities {firesTouchEvents: true}
        srcElement: html
        target: html
        targetTouches: TouchList {0: Touch, length: 1} // TouchList 接口 （所有触摸点集合）
        timeStamp: 994.1749999998137
        touches: TouchList {0: Touch, length: 1} // TouchList 接口 （所有触摸点集合）
        type: "touchmove" // 当前触摸事件类型
        view: Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
        which: 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

### # 2、Touch 接口

#### # 2.1 Touch 接口概述 （单个触摸点）

Touch 接口代表单个触摸点。触摸点可能是一根手指，也可能是一根触摸笔。

浏览器原生提供`Touch`构造函数，用来生成`Touch`实例。

    var touch = new Touch(touchOptions);

1

`Touch`构造函数接受一个配置对象作为参数，它有以下属性。

- `identifier`：必需，类型为整数，表示触摸点的唯一 ID。
- `target`：必需，类型为元素节点，表示触摸点开始时所在的网页元素。
- `clientX`：可选，类型为数值，表示触摸点相对于浏览器窗口左上角的水平距离，默认为0。
- `clientY`：可选，类型为数值，表示触摸点相对于浏览器窗口左上角的垂直距离，默认为0。
- `screenX`：可选，类型为数值，表示触摸点相对于屏幕左上角的水平距离，默认为0。
- `screenY`：可选，类型为数值，表示触摸点相对于屏幕左上角的垂直距离，默认为0。
- `pageX`：可选，类型为数值，表示触摸点相对于网页左上角的水平位置（即包括页面的滚动距离），默认为0。
- `pageY`：可选，类型为数值，表示触摸点相对于网页左上角的垂直位置（即包括页面的滚动距离），默认为0。
- `radiusX`：可选，类型为数值，表示触摸点周围受到影响的椭圆范围的 X 轴半径，默认为0。
- `radiusY`：可选：类型为数值，表示触摸点周围受到影响的椭圆范围的 Y 轴半径，默认为0。
- `rotationAngle`：可选，类型为数值，表示触摸区域的椭圆的旋转角度，单位为度数，在0到90度之间，默认值为0。
- `force`：可选，类型为数值，范围在`0`到`1`之间，表示触摸压力。`0`代表没有压力，`1`代表硬件所能识别的最大压力，默认为`0`。

#### # 2.2 Touch 接口的实例属性

##### # （1）Touch.identifier 触摸点的ID

`Touch.identifier`属性返回一个整数，表示触摸点的唯一 ID。这个值在整个触摸过程保持不变，直到触摸事件结束。

    someElement.addEventListener('touchmove', function (e) {
      for (var i = 0; i < e.changedTouches.length; i++) {
        console.log(e.changedTouches[i].identifier);
      }
    }, false);

1
2
3
4
5

##### # （2）Touch.screenX，Touch.screenY，Touch.clientX，Touch.clientY，pageX，pageY （相对屏幕、浏览器、文档的坐标）

`Touch.screenX`属性和`Touch.screenY`属性，分别表示触摸点相对于屏幕左上角的横坐标和纵坐标，与页面是否滚动无关。

`Touch.clientX`属性和`Touch.clientY`属性，分别表示触摸点相对于浏览器视口左上角的横坐标和纵坐标，与页面是否滚动无关。

`Touch.pageX`属性和`Touch.pageY`属性，分别表示触摸点相对于当前页面左上角的横坐标和纵坐标，包含了页面滚动带来的位移。

##### # （3）Touch.radiusX，Touch.radiusY，Touch.rotationAngle （触摸椭圆区域半径、角度）

`Touch.radiusX`属性和`Touch.radiusY`属性，分别返回触摸点周围受到影响的椭圆范围的 X 轴半径和 Y 轴半径，单位为像素。乘以 2 就可以得到触摸范围的宽度和高度。

`Touch.rotationAngle`属性表示触摸区域的椭圆的旋转角度，单位为度数，在`0`到`90`度之间。

上面这三个属性共同定义了用户与屏幕接触的区域，对于描述手指这一类非精确的触摸，很有帮助。指尖接触屏幕，触摸范围会形成一个椭圆，这三个属性就用来描述这个椭圆区域。

下面是一个示例。

    div.addEventListener('touchstart', rotate);
    div.addEventListener('touchmove', rotate);
    div.addEventListener('touchend', rotate);

    function rotate(e) {
      var touch = e.changedTouches.item(0);
      e.preventDefault();

      src.style.width = touch.radiusX * 2 + 'px';
      src.style.height = touch.radiusY * 2 + 'px';
      src.style.transform = 'rotate(' + touch.rotationAngle + 'deg)';
    };

1
2
3
4
5
6
7
8
9
10
11
12

##### # （4）Touch.force 触摸压力

`Touch.force`属性返回一个`0`到`1`之间的数值，表示触摸压力。`0`代表没有压力，`1`代表硬件所能识别的最大压力。

##### # （5）Touch.target 开始触摸时的元素

`Touch.target`属性返回一个元素节点，代表触摸发生时所在的那个元素节点。即使触摸点已经离开了这个节点，该属性依然不变。

### # 3、TouchList 接口

`TouchList`接口表示一组触摸点的集合。它的实例是一个类似数组的对象，成员是`Touch`的实例对象，表示所有触摸点。用户用三根手指触摸，产生的`TouchList`实例就会包含三个成员，每根手指的触摸点对应一个`Touch`实例对象。

它的实例主要通过触摸事件的`TouchEvent.touches`、`TouchEvent.changedTouches`、`TouchEvent.targetTouches`这几个属性获取。

它的实例属性和实例方法只有两个。

- `TouchList.length`：数值，表示成员数量（即触摸点的数量）。
- `TouchList.item()`：返回指定位置的成员，它的参数是该成员的位置编号（从零开始）。

### # 4、TouchEvent 接口

#### # 4.1 概述

TouchEvent 接口继承了 Event 接口，表示由触摸引发的事件实例，通常来自触摸屏或轨迹板。除了被继承的属性以外，它还有一些自己的属性。

浏览器原生提供`TouchEvent()`构造函数，用来生成触摸事件的实例。

    new TouchEvent(type, options)

1

`TouchEvent()`构造函数可以接受两个参数，第一个参数是字符串，表示事件类型；第二个参数是事件的配置对象，该参数是可选的，对象的所有属性也是可选的。除了`Event`接口的配置属性，该接口还有一些自己的配置属性。

- `touches`：`TouchList`实例，代表所有的当前处于活跃状态的触摸点，默认值是一个空数组`[]`。
- `targetTouches`：`TouchList`实例，代表所有处在触摸的目标元素节点内部、且仍然处于活动状态的触摸点，默认值是一个空数组`[]`。
- `changedTouches`：`TouchList`实例，代表本次触摸事件的相关触摸点，默认值是一个空数组`[]`。
- `ctrlKey`：布尔值，表示 Ctrl 键是否同时按下，默认值为`false`。
- `shiftKey`：布尔值，表示 Shift 键是否同时按下，默认值为`false`。
- `altKey`：布尔值，表示 Alt 键是否同时按下，默认值为`false`。
- `metaKey`：布尔值，表示 Meta 键（或 Windows 键）是否同时按下，默认值为`false`。

#### # 4.2 实例属性

TouchEvent 接口的实例具有`Event`实例的所有属性和方法，此外还有一些它自己的实例属性，这些属性全部都是只读。

##### # （1）TouchEvent.altKey，TouchEvent.ctrlKey，TouchEvent.shiftKey，TouchEvent.metaKey （是否同时按某些功能键）

- `TouchEvent.altKey`：布尔值，表示触摸时是否按下了 Alt 键。
- `TouchEvent.ctrlKey`：布尔值，表示触摸时是否按下了 Ctrl 键。
- `TouchEvent.shiftKey`：布尔值：表示触摸时是否按下了 Shift 键。
- `TouchEvent.metaKey`：布尔值，表示触摸时是否按下了 Meta 键（或 Windows 键）。

下面是一个示例。

    someElement.addEventListener('touchstart', function (e) {
      console.log('altKey = ' + e.altKey);
      console.log('ctrlKey = ' + e.ctrlKey);
      console.log('metaKey = ' + e.metaKey);
      console.log('shiftKey = ' + e.shiftKey);
    }, false);

1
2
3
4
5
6

##### # （2）TouchEvent.changedTouches （触摸点集合，不同触摸事件，含义不同）

`TouchEvent.changedTouches`属性返回一个`TouchList`实例，成员是一组`Touch`实例对象，表示本次触摸事件的相关触摸点。

对于不同的事件，该属性的含义有所不同。

- `touchstart`事件：被激活的触摸点
- `touchmove`事件：发生变化的触摸点
- `touchend`事件：消失的触摸点（即不再被触碰的点）

下面是一个示例。

    someElement.addEventListener('touchmove', function (e) {
      for (var i = 0; i < e.changedTouches.length; i++) {
        console.log(e.changedTouches[i].identifier);
      }
    }, false);

1
2
3
4
5

##### # （3）TouchEvent.touches （仍然活动的触摸点集合）

`TouchEvent.touches`属性返回一个`TouchList`实例，成员是所有仍然处于活动状态（即触摸中）的触摸点。一般来说，一个手指就是一个触摸点。

下面是一个示例。

    someElement.addEventListener('touchstart', function (e) {
      switch (e.touches.length) {
        // 一根手指触摸
        case 1: handle_one_touch(e); break;
        // 两根手指触摸
        case 2: handle_two_touches(e); break;
        // 三根手指触摸
        case 3: handle_three_touches(e); break;
        // 其他情况
        default: console.log('Not supported'); break;
      }
    }, false);

1
2
3
4
5
6
7
8
9
10
11
12

##### # （4）TouchEvent.targetTouches （目标元素内活动的触摸点集合）

`TouchEvent.targetTouches`属性返回一个`TouchList`实例，成员是触摸事件的目标元素节点内部、所有仍然处于活动状态（即触摸中）的触摸点。

    function touches_in_target(ev) {
      return (ev.touches.length === ev.targetTouches.length ? true : false);
    }

1
2
3

上面代码用来判断，是否所有触摸点都在目标元素内。

### # 5、触摸事件的种类

触摸引发的事件，有以下几种。可以通过`TouchEvent.type`属性，查看到底发生的是哪一种事件。

- `touchstart`：用户开始触摸时触发，它的`target`属性返回发生触摸的元素节点。【开始触摸】
- `touchend`：用户不再接触触摸屏时（或者移出屏幕边缘时）触发，它的`target`属性与`touchstart`事件一致的，就是开始触摸时所在的元素节点。它的`changedTouches`属性返回一个`TouchList`实例，包含所有不再触摸的触摸点（即`Touch`实例对象）。【触摸结束】
- `touchmove`：用户移动触摸点时触发，它的`target`属性与`touchstart`事件一致。如果触摸的半径、角度、力度发生变化，也会触发该事件。【触摸移动中】
- `touchcancel`：触摸点取消时触发，比如在触摸区域跳出一个模态窗口（modal window）、触摸点离开了文档区域（进入浏览器菜单栏）、用户的触摸点太多，超过了支持的上限（自动取消早先的触摸点）。【触摸点被取消】

下面是一个例子。

    var el = document.getElementsByTagName('canvas')[0];
    el.addEventListener('touchstart', handleStart, false);
    el.addEventListener('touchmove', handleMove, false);

    function handleStart(evt) {
      evt.preventDefault();
      var touches = evt.changedTouches;
      for (var i = 0; i < touches.length; i++) {
        console.log(touches[i].pageX, touches[i].pageY);
      }
    }

    function handleMove(evt) {
      evt.preventDefault();
      var touches = evt.changedTouches;
      for (var i = 0; i < touches.length; i++) {
        var touch = touches[i];
        console.log(touch.pageX, touch.pageY);
      }
    }

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

## # 九、拖拉事件

### # 1、拖拉事件的种类

拖拉（drag）指的是，用户在某个对象上按下鼠标键不放，拖动它到另一个位置，然后释放鼠标键，将该对象放在那里。

拖拉的对象有好几种，包括元素节点、图片、链接、选中的文字等等。在网页中，除了元素节点默认不可以拖拉，其他（图片、链接、选中的文字）都是可以直接拖拉的。为了让元素节点可拖拉，可以将该节点的`draggable`属性设为`true`。

    <div draggable="true">
      此区域可拖拉
    </div>

1
2
3

`draggable`属性可用于任何元素节点，但是图片（`<img>`）和链接（`<a>`）不加这个属性，就可以拖拉。对于它们，用到这个属性的时候，往往是将其设为`false`，防止拖拉这两种元素。

注意，一旦某个元素节点的`draggable`属性设为`true`，就无法再用鼠标选中该节点内部的文字或子节点了。

当元素节点或选中的文本被拖拉时，就会持续触发拖拉事件，包括以下一些事件。

拖拉的节点上触发：

- `drag`：拖拉过程中，在被拖拉的节点上持续触发（相隔几百毫秒）。
- `dragstart`：用户开始拖拉时，在被拖拉的节点上触发，该事件的`target`属性是被拖拉的节点。通常应该在这个事件的监听函数中，指定拖拉的数据。
- `dragend`：拖拉结束时（释放鼠标键或按下 ESC 键）在被拖拉的节点上触发，该事件的`target`属性是被拖拉的节点。它与`dragstart`事件，在同一个节点上触发。不管拖拉是否跨窗口，或者中途被取消，`dragend`事件总是会触发的。

拖拉到别的节点上触发：

- `dragenter`：拖拉进入当前节点时，在当前节点上触发一次，该事件的`target`属性是当前节点。通常应该在这个事件的监听函数中，指定是否允许在当前节点放下（drop）拖拉的数据。如果当前节点没有该事件的监听函数，或者监听函数不执行任何操作，就意味着不允许在当前节点放下数据。在视觉上显示拖拉进入当前节点，也是在这个事件的监听函数中设置。

- `dragover`：拖拉到当前节点上方时，在当前节点上持续触发（相隔几百毫秒），该事件的`target`属性是当前节点。该事件与`dragenter`事件的区别是，`dragenter`事件在进入该节点时触发，然后只要没有离开这个节点，`dragover`事件会持续触发。

- `dragleave`：拖拉操作离开当前节点范围时，在当前节点上触发，该事件的`target`属性是当前节点。如果要在视觉上显示拖拉离开操作当前节点，就在这个事件的监听函数中设置。

- `drop`：被拖拉的节点或选中的文本，释放到目标节点时，在目标节点上触发。注意，如果当前节点不允许`drop`，即使在该节点上方松开鼠标键，也不会触发该事件。如果用户按下 ESC 键，取消这个操作，也不会触发该事件。该事件的监听函数负责取出拖拉数据，并进行相关处理。

下面的例子展示，如何动态改变被拖动节点的背景色。

    div.addEventListener('dragstart', function (e) {
      this.style.backgroundColor = 'red';
    }, false);

    div.addEventListener('dragend', function (e) {
      this.style.backgroundColor = 'green';
    }, false);

1
2
3
4
5
6
7

上面代码中，`div`节点被拖动时，背景色会变为红色，拖动结束，又变回绿色。

    <div class="div1" draggable="true">
        div1，此区域可拖拉
    </div>

    <div class="div2">
        div2
    </div>

    <script>
        var div1 = document.querySelector('.div1');
        var div2 = document.querySelector('.div2');

        div1.addEventListener('dragstart', function (e){
            console.log('开始拖拉')
        });
        div1.addEventListener('drag', function (e) {
            console.log('拖拉中') // 持续触发
        });
        div1.addEventListener('dragend', function (e){
            console.log('结束拖拉')
        });

        div2.addEventListener('dragenter', function(){
            console.log('拖到了div2')
        })
        div2.addEventListener('dragover', function(){
            //console.log('正在div2上方') // 持续触发
        })
        div2.addEventListener('dragleave', function(){
            console.log('离开div2')
        })
        div2.addEventListener('drop', function() {
            console.log('在div2上释放')
        })
    </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
25
26
27
28
29
30
31
32
33
34
35
36

下面是一个例子，展示如何实现将一个节点从当前父节点，拖拉到另一个父节点中。

    /* HTML 代码如下
     <div class="dropzone">
       <div id="draggable" draggable="true">
         该节点可拖拉
       </div>
     </div>
     <div class="dropzone"></div>
     <div class="dropzone"></div>
     <div class="dropzone"></div>
    */

    // 被拖拉节点
    var dragged;

    document.addEventListener('dragstart', function (event) {
      // 保存被拖拉节点
      dragged = event.target;
      // 被拖拉节点的背景色变透明
      event.target.style.opacity = 0.5;
    }, false);

    document.addEventListener('dragend', function (event) {
      // 被拖拉节点的背景色恢复正常
      event.target.style.opacity = '';
    }, false);

    document.addEventListener('dragover', function (event) {
      // 防止拖拉效果被重置，允许被拖拉的节点放入目标节点
      event.preventDefault();
    }, false);

    document.addEventListener('dragenter', function (event) {
      // 目标节点的背景色变紫色
      // 由于该事件会冒泡，所以要过滤节点
      if (event.target.className === 'dropzone') {
        event.target.style.background = 'purple';
      }
    }, false);

    document.addEventListener('dragleave', function( event ) {
      // 目标节点的背景色恢复原样
      if (event.target.className === 'dropzone') {
        event.target.style.background = '';
      }
    }, false);

    document.addEventListener('drop', function( event ) {
      // 防止事件默认行为（比如某些元素节点上可以打开链接），
      event.preventDefault();
      if (event.target.className === 'dropzone') {
        // 恢复目标节点背景色
        event.target.style.background = '';
        // 将被拖拉节点插入目标节点
        dragged.parentNode.removeChild(dragged);
        event.target.appendChild( dragged );
      }
    }, false);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

关于拖拉事件，有以下几个注意点。

- 拖拉过程只触发以上这些拖拉事件，尽管鼠标在移动，但是鼠标事件不会触发。
- 将文件从操作系统拖拉进浏览器，不会触发`dragstart`和`dragend`事件。
- `dragenter`和`dragover`事件的监听函数，用来取出拖拉的数据（即允许放下被拖拉的元素）。由于网页的大部分区域不适合作为放下拖拉元素的目标节点，所以这两个事件的默认设置为当前节点不允许接受被拖拉的元素。如果想要在目标节点上放下的数据，首先必须阻止这两个事件的默认行为。

    <div ondragover="return false">
    <div ondragover="event.preventDefault()">

1
2

上面代码中，如果不取消拖拉事件或者阻止默认行为，就不能在`div`节点上放下被拖拉的节点。

### # 2、DragEvent 接口

拖拉事件都继承了`DragEvent`接口，这个接口又继承了`MouseEvent`接口和`Event`接口。

浏览器原生提供一个`DragEvent()`构造函数，用来生成拖拉事件的实例对象。

    new DragEvent(type, options)

1

`DragEvent()`构造函数接受两个参数，第一个参数是字符串，表示事件的类型，该参数必须；第二个参数是事件的配置对象，用来设置事件的属性，该参数可选。配置对象除了接受`MouseEvent`接口和`Event`接口的配置属性，还可以设置`dataTransfer`属性要么是`null`，要么是一个`DataTransfer`接口的实例。

`DataTransfer`的实例对象用来读写拖拉事件中传输的数据，详见下文《DataTransfer 接口》的部分。

### # 3、DataTransfer 接口概述

所有拖拉事件的实例都有一个`DragEvent.dataTransfer`属性，用来读写需要传递的数据。这个属性的值是一个`DataTransfer`接口的实例。

浏览器原生提供一个`DataTransfer()`构造函数，用来生成`DataTransfer`实例对象。

    var dataTrans = new DataTransfer();

1

`DataTransfer()`构造函数不接受参数。

拖拉的数据分成两方面：数据的种类（又称格式）和数据的值。数据的种类是一个 MIME 字符串（比如`text/plain`、`image/jpeg`），数据的值是一个字符串。一般来说，如果拖拉一段文本，则数据默认就是那段文本；如果拖拉一个链接，则数据默认就是链接的 URL。

拖拉事件开始时，开发者可以提供数据类型和数据值。拖拉过程中，开发者通过`dragenter`和`dragover`事件的监听函数，检查数据类型，以确定是否允许放下（drop）被拖拉的对象。比如，在只允许放下链接的区域，检查拖拉的数据类型是否为`text/uri-list`。

发生`drop`事件时，监听函数取出拖拉的数据，对其进行处理。

### # 4、DataTransfer 的实例属性

#### # 4.1 DataTransfer.dropEffect 设置接受拖拉的区域的效果

`DataTransfer.dropEffect`属性用来设置放下（drop）被拖拉节点时的效果，会影响到拖拉经过相关区域时鼠标的形状。它可能取下面的值。

- copy：复制被拖拉的节点
- move：移动被拖拉的节点
- link：创建指向被拖拉的节点的链接
- none：无法放下被拖拉的节点

除了上面这些值，设置其他的值都是无效的。

    target.addEventListener('dragover', function (e) {
      e.preventDefault();
      e.stopPropagation();
      e.dataTransfer.dropEffect = 'copy';
    });

1
2
3
4
5

上面代码中，被拖拉元素一旦`drop`，接受的区域会复制该节点。

`dropEffect`属性一般在`dragenter`和`dragover`事件的监听函数中设置，对于`dragstart`、`drag`、`dragleave`这三个事件，该属性不起作用。因为该属性只对接受被拖拉的节点的区域有效，对被拖拉的节点本身是无效的。进入目标区域后，拖拉行为会初始化成设定的效果。

#### # 4.2 DataTransfer.effectAllowed 设置被拖拉的节点允许的效果

`DataTransfer.effectAllowed`属性设置本次拖拉中允许的效果。它可能取下面的值。

- copy：复制被拖拉的节点
- move：移动被拖拉的节点
- link：创建指向被拖拉节点的链接
- copyLink：允许`copy`或`link`
- copyMove：允许`copy`或`move`
- linkMove：允许`link`或`move`
- all：允许所有效果
- none：无法放下被拖拉的节点
- uninitialized：默认值，等同于`all`

如果某种效果是不允许的，用户就无法在目标节点中达成这种效果。

这个属性与`dropEffect`属性是同一件事的两个方面。前者设置被拖拉的节点允许的效果，后者设置接受拖拉的区域的效果，它们往往配合使用。

`dragstart`事件的监听函数，可以用来设置这个属性。其他事件的监听函数里面设置这个属性是无效的。

    source.addEventListener('dragstart', function (e) { // 被拖拉节点上设置
      e.dataTransfer.effectAllowed = 'move';
    });

    target.addEventListener('dragover', function (e) { // 接受区域节点上设置
      e.dataTransfer.dropEffect = 'move';
    });

1
2
3
4
5
6
7

只要`dropEffect`属性和`effectAllowed`属性之中，有一个为`none`，就无法在目标节点上完成`drop`操作。

#### # 4.3 DataTransfer.files 本地文件

`DataTransfer.files`属性是一个 FileList 对象，包含一组本地文件，可以用来在拖拉操作中传送。如果本次拖拉不涉及文件，则该属性为空的 FileList 对象。

##### # 例子：接收拖拉文件

    // HTML 代码如下
    // <div id="output" style="min-height: 200px;border: 1px solid black;">
    //   文件拖拉到这里
    // </div>

    var div = document.getElementById('output');

    div.addEventListener("dragenter", function( event ) {
      div.textContent = '';
      event.stopPropagation();
      event.preventDefault();
    }, false);

    div.addEventListener("dragover", function( event ) {
      event.stopPropagation();
      event.preventDefault();
    }, false);

    div.addEventListener("drop", function( event ) {
      event.stopPropagation();
      event.preventDefault();
      var files = event.dataTransfer.files;
      for (var i = 0; i < files.length; i++) {
        div.textContent += files[i].name + ' ' + files[i].size + '字节\n';
      }
    }, false);

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

上面代码中，通过`dataTransfer.files`属性读取被拖拉的文件的信息。如果想要读取文件内容，就要使用`FileReader`对象。

    div.addEventListener('drop', function(e) {
      e.preventDefault();
      e.stopPropagation();

      var fileList = e.dataTransfer.files;
      if (fileList.length > 0) {
        var file = fileList[0];
        var reader = new FileReader();
        reader.readAsDataURL(file);// 解析为url
        reader.onloadend = function(e) {
          if (e.target.readyState === FileReader.DONE) {
            var content = reader.result;
            div.innerHTML = '<img src="'+ content +'"> File: ' + file.name + '\n\n' + content;
          }
        }
        reader.readAsBinaryString(file);
      }
    });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

#### # 4.4 DataTransfer.types 数据格式

`DataTransfer.types`属性是一个只读的数组，每个成员是一个字符串，里面是拖拉的数据格式（通常是 MIME 值）。比如，如果拖拉的是文字，对应的成员就是`text/plain`。

下面是一个例子，通过检查`dataTransfer`属性的类型，决定是否允许在当前节点执行`drop`操作。

    function contains(list, value){
      for (var i = 0; i < list.length; ++i) {
        if(list[i] === value) return true;
      }
      return false;
    }

    function doDragOver(event) {
      var isLink = contains(event.dataTransfer.types, 'text/uri-list');
      if (isLink) event.preventDefault();
    }

1
2
3
4
5
6
7
8
9
10
11

上面代码中，只有当被拖拉的节点是一个链接时，才允许在当前节点放下。

#### # 4.5 DataTransfer.items

`DataTransfer.items`属性返回一个类似数组的只读对象（DataTransferItemList 实例），每个成员就是本次拖拉的一个对象（DataTransferItem 实例）。如果本次拖拉不包含对象，则返回一个空对象。

DataTransferItemList 实例具有以下的属性和方法。

- `length`：返回成员的数量
- `add(data, type)`：增加一个指定内容和类型（比如`text/html`和`text/plain`）的字符串作为成员
- `add(file)`：`add`方法的另一种用法，增加一个文件作为成员
- `remove(index)`：移除指定位置的成员
- `clear()`：移除所有的成员

DataTransferItem 实例具有以下的属性和方法。

- `kind`：返回成员的种类（`string`还是`file`）。
- `type`：返回成员的类型（通常是 MIME 值）。
- `getAsFile()`：如果被拖拉是文件，返回该文件，否则返回`null`。
- `getAsString(callback)`：如果被拖拉的是字符串，将该字符传入指定的回调函数处理。该方法是异步的，所以需要传入回调函数。

下面是一个例子。

    div.addEventListener('drop', function (e) {
      e.preventDefault();
      if (e.dataTransfer.items != null) {
        for (var i = 0; i < e.dataTransfer.items.length; i++) {
          console.log(e.dataTransfer.items[i].kind + ': ' + e.dataTransfer.items[i].type);
        }
      }
    });

1
2
3
4
5
6
7
8

### # 5、DataTransfer 的实例方法

#### # 5.1 DataTransfer.setData() 设置拖拉事件所带有的数据

`DataTransfer.setData()`方法用来设置拖拉事件所带有的数据。该方法没有返回值。

    event.dataTransfer.setData('text/plain', 'Text to drag');

1

上面代码为当前的拖拉事件加入纯文本数据。

该方法接受两个参数，都是字符串。第一个参数表示数据类型（比如`text/plain`），第二个参数是具体数据。如果指定类型的数据在`dataTransfer`属性不存在，那么这些数据将被加入，否则原有的数据将被新数据替换。

如果是拖拉文本框或者拖拉选中的文本，会默认将对应的文本数据，添加到`dataTransfer`属性，不用手动指定。

    <div draggable="true">
      aaa
    </div>

1
2
3

上面代码中，拖拉这个`<div>`元素会自动带上文本数据`aaa`。

使用`setData`方法，可以替换到原有数据。

    <div
      draggable="true"
      ondragstart="event.dataTransfer.setData('text/plain', 'bbb')"
    >
      aaa
    </div>

1
2
3
4
5
6

上面代码中，拖拉数据实际上是`bbb`，而不是`aaa`。

下面是添加其他类型的数据。由于`text/plain`是最普遍支持的格式，为了保证兼容性，建议最后总是保存一份纯文本格式的数据。

    var dt = event.dataTransfer;

    // 添加链接
    dt.setData('text/uri-list', 'http://www.example.com');
    dt.setData('text/plain', 'http://www.example.com');

    // 添加 HTML 代码
    dt.setData('text/html', 'Hello there, <strong>stranger</strong>');
    dt.setData('text/plain', 'Hello there, <strong>stranger</strong>');

    // 添加图像的 URL
    dt.setData('text/uri-list', imageurl);
    dt.setData('text/plain', imageurl);

1
2
3
4
5
6
7
8
9
10
11
12
13

可以一次提供多种格式的数据。

    var dt = event.dataTransfer;
    dt.setData('application/x-bookmark', bookmarkString);
    dt.setData('text/uri-list', 'http://www.example.com');
    dt.setData('text/plain', 'http://www.example.com');

1
2
3
4

上面代码中，通过在同一个事件上面，存放三种类型的数据，使得拖拉事件可以在不同的对象上面，`drop`不同的值。注意，第一种格式是一个自定义格式，浏览器默认无法读取，这意味着，只有某个部署了特定代码的节点，才可能`drop`（读取到）这个数据。

#### # 5.2 DataTransfer.getData() 返回指定类型的数据

`DataTransfer.getData()`方法接受一个字符串（表示数据类型）作为参数，返回事件所带的指定类型的数据（通常是用`setData`方法添加的数据）。如果指定类型的数据不存在，则返回空字符串。通常只有`drop`事件触发后，才能取出数据。

下面是一个`drop`事件的监听函数，用来取出指定类型的数据。

    function onDrop(event) {
      var data = event.dataTransfer.getData('text/plain');
      event.target.textContent = data;
      event.preventDefault();
    }

1
2
3
4
5

上面代码取出拖拉事件的文本数据，将其替换成当前节点的文本内容。注意，这时还必须取消浏览器的默认行为，因为假如用户拖拉的是一个链接，浏览器默认会在当前窗口打开这个链接。

`getData`方法返回的是一个字符串，如果其中包含多项数据，就必须手动解析。

    function doDrop(event) {
      var lines = event.dataTransfer.getData('text/uri-list').split('\n');
      for (let line of lines) {
        let link = document.createElement('a');
        link.href = line;
        link.textContent = line;
        event.target.appendChild(link);
      }
      event.preventDefault();
    }

1
2
3
4
5
6
7
8
9
10

上面代码中，`getData`方法返回的是一组链接，就必须自行解析。

类型值指定为`URL`，可以取出第一个有效链接。

    var link = event.dataTransfer.getData('URL');

1

下面的例子是从多种类型的数据里面取出数据。

    function doDrop(event) {
      var types = event.dataTransfer.types;
      var supportedTypes = ['text/uri-list', 'text/plain'];
      types = supportedTypes.filter(function (value) { types.includes(value) });
      if (types.length) {
        var data = event.dataTransfer.getData(types[0]);
      }
      event.preventDefault();
    }

1
2
3
4
5
6
7
8
9

#### # 5.3 DataTransfer.clearData() 清除指定或全部数据

`DataTransfer.clearData()`方法接受一个字符串（表示数据类型）作为参数，删除事件所带的指定类型的数据。如果没有指定类型，则删除所有数据。如果指定类型不存在，则调用该方法不会产生任何效果。

    event.dataTransfer.clearData('text/uri-list');

1

上面代码清除事件所带的`text/uri-list`类型的数据。

该方法不会移除拖拉的文件，因此调用该方法后，`DataTransfer.types`属性可能依然会返回`Files`类型（前提是存在文件拖拉）。

注意，该方法只能在`dragstart`事件的监听函数之中使用，因为这是拖拉操作的数据唯一可写的时机。

#### # 5.4 DataTransfer.setDragImage() 设置拖动过程中的图片

拖动过程中（`dragstart`事件触发后），浏览器会显示一张图片跟随鼠标一起移动，表示被拖动的节点。这张图片是自动创造的，通常显示为被拖动节点的外观，不需要自己动手设置。

`DataTransfer.setDragImage()`方法可以自定义这张图片。它接受三个参数。第一个是`<img>`节点或者`<canvas>`节点，如果省略或为`null`，则使用被拖动的节点的外观；第二个和第三个参数为鼠标相对于该图片左上角的横坐标和右坐标。

下面是一个例子。

    /* HTML 代码如下
     <div id="drag-with-image" class="dragdemo" draggable="true">
       drag me
     </div>
    */

    var div = document.getElementById('drag-with-image');
    div.addEventListener('dragstart', function (e) {
      var img = document.createElement('img');
      img.src = 'http://path/to/img';
      e.dataTransfer.setDragImage(img, 0, 0);
    }, false);

1
2
3
4
5
6
7
8
9
10
11
12

## # 十、其他常见事件

### # 1、资源事件

#### # 1.1 beforeunload 事件 （关闭窗口前调用）

`beforeunload`事件在窗口、文档、各种资源将要卸载前触发。它可以用来防止用户不小心卸载资源。

如果该事件对象的`returnValue`属性是一个非空字符串，那么浏览器就会弹出一个对话框，询问用户是否要卸载该资源。但是，用户指定的字符串可能无法显示，浏览器会展示预定义的字符串。如果用户点击“取消”按钮，资源就不会卸载。

    window.addEventListener('beforeunload', function (event) {
      event.returnValue = '你确定离开吗？';
    });

1
2
3

上面代码中，用户如果关闭窗口，浏览器会弹出一个窗口，要求用户确认。

浏览器对这个事件的行为很不一致，有的浏览器调用`event.preventDefault()`，也会弹出对话框。IE 浏览器需要显式返回一个非空的字符串，才会弹出对话框。而且，大多数浏览器在对话框中不显示指定文本，只显示默认文本。因此，可以采用下面的写法，取得最大的兼容性。

    window.addEventListener('beforeunload', function (e) {
      var confirmationMessage = '确认关闭窗口？';

      e.returnValue = confirmationMessage;
      return confirmationMessage;
    });

1
2
3
4
5
6

注意，许多手机浏览器默认忽略这个事件，桌面浏览器也有办法忽略这个事件。所以，它可能根本不会生效，不能依赖它来阻止用户关闭窗口。另外，一旦使用了`beforeunload`事件，浏览器就不会缓存当前网页，使用“回退”按钮将重新向服务器请求网页。这是因为监听这个事件的目的，一般是修改初始状态，这时缓存初始页面就没意义了。

基本上，只有一种场合可以监听`unload`事件，其他情况都不应该监听：用户修改了表单，还没有保存就要离开。

#### # 1.2 unload 事件 （即将关闭窗口时调用）

`unload`事件在窗口关闭或者`document`对象将要卸载时触发。它的触发顺序排在`beforeunload`、`pagehide`事件后面。

`unload`事件发生时，文档处于一个特殊状态。所有资源依然存在，但是对用户来说都不可见，UI 互动全部无效。这个事件是无法取消的，即使在监听函数里面抛出错误，也不能停止文档的卸载。

    window.addEventListener('unload', function(event) {
      console.log('文档将要卸载');
    });

1
2
3

手机上，浏览器或系统可能会直接丢弃网页，这时该事件根本不会发生。而且跟`beforeunload`事件一样，一旦使用了`unload`事件，浏览器就不会缓存当前网页，理由同上。因此，任何情况下都不应该依赖这个事件，指定网页卸载时要执行的代码，可以考虑完全不使用这个事件。

#### # 1.3 load 事件，error 事件 ，abort事件 （页面或某个资源加载成功/失败/取消时调用）

`load`事件在页面或某个资源加载成功时触发。注意，页面或资源从浏览器缓存加载，并不会触发`load`事件。

    window.addEventListener('load', function(event) {
      console.log('所有资源都加载完成');
    });

1
2
3

`error`事件是在页面或资源加载失败时触发。

`abort`事件是在用户取消加载时触发。

这三个事件实际上属于进度事件，不仅发生在`document`对象，还发生在各种外部资源上面。浏览网页就是一个加载各种资源的过程，图像（image）、样式表（style sheet）、脚本（script）、视频（video）、音频（audio）、Ajax请求（XMLHttpRequest）等等。这些资源和`document`对象、`window`对象、XMLHttpRequestUpload 对象，都会触发`load`事件和`error`事件。

触发这三个事件的对象有：

- window
- document
- body
- img
- style
- script
- video
- audio
- Ajax
- 等等

### # 2、session 历史事件

#### # 2.1 pageshow 事件（页面显示，加载页面后执行），pagehide 事件（退出当前页面触发）

默认情况下，浏览器会在当前会话（session）缓存页面，当用户点击“前进/后退”按钮时，浏览器就会从缓存中加载页面。

pageshow 事件在页面加载时触发，包括第一次加载和从缓存加载两种情况。如果要指定页面每次加载（不管是不是从浏览器缓存）时都运行的代码，可以放在这个事件的监听函数。

第一次加载时，它的触发顺序排在`load`事件后面。从缓存加载时，`load`事件不会触发，因为网页在缓存中的样子通常是`load`事件的监听函数运行后的样子，所以不必重复执行。同理，如果是从缓存中加载页面，网页内初始化的 JavaScript 脚本（比如 DOMContentLoaded 事件的监听函数）也不会执行。

    window.addEventListener('pageshow', function(event) {
      console.log('pageshow: ', event);
    });

1
2
3

pageshow 事件有一个`persisted`属性，返回一个布尔值。页面第一次加载时，这个属性是`false`；当页面从缓存加载时，这个属性是`true`。

    window.addEventListener('pageshow', function(event){
      if (event.persisted) { // 是否从缓存加载
        // ...
      }
    });

1
2
3
4
5

`pagehide`事件与`pageshow`事件类似，当用户通过“前进/后退”按钮，离开当前页面时触发。它与 unload 事件的区别在于，如果在 window 对象上定义`unload`事件的监听函数之后，页面不会保存在缓存中，而使用`pagehide`事件，页面会保存在缓存中。

`pagehide`事件实例也有一个`persisted`属性，将这个属性设为`true`，就表示页面要保存在缓存中；设为`false`，表示网页不保存在缓存中，这时如果设置了unload 事件的监听函数，该函数将在 pagehide 事件后立即运行。

如果页面包含`<frame>`或`<iframe>`元素，则`<frame>`页面的`pageshow`事件和`pagehide`事件，都会在主页面之前触发。

注意，这两个事件只在浏览器的`history`（历史）对象发生变化时触发，跟网页是否可见没有关系。

#### # 2.2 popstate 事件（在浏览器的`history`对象的当前记录发生显式切换时触发）

`popstate`事件在浏览器的`history`对象的当前记录发生显式切换时触发。注意，调用`history.pushState()`或`history.replaceState()`，并不会触发`popstate`事件。该事件只在用户在`history`记录之间显式切换时触发，比如鼠标点击“后退/前进”按钮，或者在脚本中调用`history.back()`、`history.forward()`、`history.go()`时触发。

该事件对象有一个`state`属性，保存`history.pushState`方法和`history.replaceState`方法为当前记录添加的`state`对象。

    window.onpopstate = function (event) {
      console.log('state: ' + event.state);
    };
    history.pushState({page: 1}, 'title 1', '?page=1');
    history.pushState({page: 2}, 'title 2', '?page=2');
    history.replaceState({page: 3}, 'title 3', '?page=3');
    history.back(); // state: {"page":1}
    history.back(); // state: null
    history.go(2);  // state: {"page":3}

1
2
3
4
5
6
7
8
9

上面代码中，`pushState`方法向`history`添加了两条记录，然后`replaceState`方法替换掉当前记录。因此，连续两次`back`方法，会让当前条目退回到原始网址，它没有附带`state`对象，所以事件的`state`属性为`null`，然后前进两条记录，又回到`replaceState`方法添加的记录。

浏览器对于页面首次加载，是否触发`popstate`事件，处理不一样，Firefox 不触发该事件。

#### # 2.3 hashchange 事件 （hash发生变化时触发）

`hashchange`事件在 URL 的 hash 部分（即`#`号后面的部分，包括`#`号）发生变化时触发。该事件一般在`window`对象上监听。

`hashchange`的事件实例具有两个特有属性：`oldURL`属性和`newURL`属性，分别表示变化前后的完整 URL。

    // URL 是 http://www.example.com/
    window.addEventListener('hashchange', myFunction);

    function myFunction(e) {
      console.log(e.oldURL);
      console.log(e.newURL);
    }

    location.hash = 'part2';
    // http://www.example.com/
    // http://www.example.com/#part2

1
2
3
4
5
6
7
8
9
10
11

### # 3、网页状态事件

#### # 3.1 DOMContentLoaded 事件 （DOM内容加载完成后触发）

网页下载并解析完成以后，浏览器就会在`document`对象上触发 DOMContentLoaded 事件。这时，仅仅完成了网页的解析（整张页面的 DOM 生成了），所有外部资源（样式表、脚本、iframe 等等）可能还没有下载结束。也就是说，这个事件比`load`事件，发生时间早得多。

    document.addEventListener('DOMContentLoaded', function (event) {
      console.log('DOM生成');
    });

1
2
3

注意，网页的 JavaScript 脚本是同步执行的，脚本一旦发生堵塞，将推迟触发`DOMContentLoaded`事件。

    document.addEventListener('DOMContentLoaded', function (event) {
      console.log('DOM 生成');
    });

    // 这段代码会推迟触发 DOMContentLoaded 事件
    for(var i = 0; i < 1000000000; i++) {
      // ...
    }

1
2
3
4
5
6
7
8

#### # 3.2 readystatechange 事件（Document 对象和 XMLHttpRequest 对象的`readyState`属性发生变化时触发）

`readystatechange`事件当 Document 对象和 XMLHttpRequest 对象的`readyState`属性发生变化时触发。`document.readyState`有三个可能的值：`loading`（网页正在加载）、`interactive`（网页已经解析完成，但是外部资源仍然处在加载状态）和`complete`（网页和所有外部资源已经结束加载，`load`事件即将触发）。

    document.onreadystatechange = function () {
      if (document.readyState === 'interactive') {
        // ...
      }
    }

1
2
3
4
5

这个事件可以看作`DOMContentLoaded`事件的另一种实现方法。

### # 4、窗口事件

#### # 4.1 scroll 事件 （文档或文档元素滚动时触发）

`scroll`事件在文档或文档元素滚动时触发，主要出现在用户拖动滚动条。

    window.addEventListener('scroll', callback);

1

该事件会连续地大量触发，所以它的监听函数之中不应该有非常耗费计算的操作。推荐的做法是使用`requestAnimationFrame`或`setTimeout`控制该事件的触发频率，然后可以结合`customEvent`抛出一个新事件。

    (function () {
      var throttle = function (type, name, obj) { // 节流函数 控制触发频率
        var obj = obj || window;
        var running = false;
        var func = function () {
          if (running) { return; }
          running = true;
          requestAnimationFrame(function() { // 结合此方法控制在每秒60次
            obj.dispatchEvent(new CustomEvent(name));
            running = false;
          });
        };
        obj.addEventListener(type, func);
      };

      // 将 scroll 事件重定义为 optimizedScroll 事件
      throttle('scroll', 'optimizedScroll');
    })();

    window.addEventListener('optimizedScroll', function() {
      console.log('Resource conscious scroll callback!');
    });

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

上面代码中，`throttle`函数用于控制事件触发频率，`requestAnimationFrame`方法保证每次页面重绘（每秒60次），只会触发一次`scroll`事件的监听函数。也就是说，上面方法将`scroll`事件的触发频率，限制在每秒60次。具体来说，就是`scroll`事件只要频率低于每秒60次，就会触发`optimizedScroll`事件，从而执行`optimizedScroll`事件的监听函数。

改用`setTimeout`方法，可以放置更大的时间间隔。

    (function() {
      window.addEventListener('scroll', scrollThrottler, false);

      var scrollTimeout;
      function scrollThrottler() {
        if (!scrollTimeout) {
          scrollTimeout = setTimeout(function () {
            scrollTimeout = null;
            actualScrollHandler();
          }, 66);
        }
      }

      function actualScrollHandler() {
        // ...
      }
    }());

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

上面代码中，每次`scroll`事件都会执行`scrollThrottler`函数。该函数里面有一个定时器`setTimeout`，每66毫秒触发一次（每秒15次）真正执行的任务`actualScrollHandler`。

下面是一个更一般的`throttle`函数的写法。

    function throttle(fn, wait) {
      var time = Date.now();
      return function() {
        if ((time + wait - Date.now()) < 0) {
          fn();
          time = Date.now();
        }
      }
    }

    window.addEventListener('scroll', throttle(callback, 1000));

1
2
3
4
5
6
7
8
9
10
11

上面的代码将`scroll`事件的触发频率，限制在一秒一次。

lodash
(opens new window)函数库提供了现成的`throttle`函数，可以直接使用。

    window.addEventListener('scroll', _.throttle(callback, 1000));

1

本书前面介绍过`debounce`的概念，`throttle`与它区别在于，`throttle`是“节流”，确保一段时间内只执行一次，而`debounce`是“防抖”，要连续操作结束后再执行。以网页滚动为例，`debounce`要等到用户停止滚动后才执行，`throttle`则是如果用户一直在滚动网页，那么在滚动过程中还是会执行。

#### # 4.2 resize 事件（窗口大小变化时触发）

`resize`事件在改变浏览器窗口大小时触发，主要发生在`window`对象上面。

    var resizeMethod = function () {
      if (document.body.clientWidth < 768) {
        console.log('移动设备的视口');
      }
    };

    window.addEventListener('resize', resizeMethod, true);

1
2
3
4
5
6
7

该事件也会连续地大量触发，所以最好像上面的`scroll`事件一样，通过`throttle`函数控制事件触发频率。

#### # 4.3 fullscreenchange 事件（元素进入/退出全屏时触发），fullscreenerror 事件（无法切换全屏时触发）

`fullscreenchange`事件在元素进入或退出全屏状态时触发，该事件发生在`document`对象上面。

注意，此事件非浏览器的进入/退出全屏时触发的事件。

    document.addEventListener('fullscreenchange', function (event) {
        console.log(document.fullscreenElement); // 全屏的元素，如果为null时表示已退出全屏
    });
    document.addEventListener('click', function (event) {
        document.querySelector('div').requestFullscreen(); // 设置元素为全屏展示
    });

1
2
3
4
5
6

`fullscreenerror`事件在浏览器无法切换到全屏状态时触发。

### # 5、剪贴板事件

以下三个事件属于剪贴板操作的相关事件。

- `cut`：将选中的内容从文档中移除，加入剪贴板时触发。【剪切】
- `copy`：进行复制动作时触发。【拷贝】
- `paste`：剪贴板内容粘贴到文档后触发。【粘贴】

这三个事件都是**`ClipboardEvent`接口的实例**。`ClipboardEvent`有一个实例属性`clipboardData`，是一个 DataTransfer 对象，存放剪贴的数据。具体的 API 接口和操作方法，请参见《拖拉事件》的 DataTransfer 对象部分。

    document.addEventListener('copy', function (e) {
      e.clipboardData.setData('text/plain', 'Hello, world!');
      e.clipboardData.setData('text/html', '<b>Hello, world!</b>');
      e.preventDefault();
    });

1
2
3
4
5

上面的代码使得复制进入剪贴板的，都是开发者指定的数据，而不是用户想要拷贝的数据。

### # 6、焦点事件

焦点事件发生在元素节点和`document`对象上面，与获得或失去焦点相关。它主要包括以下四个事件。

- `focus`：元素节点获得焦点后触发，该事件不会冒泡。
- `blur`：元素节点失去焦点后触发，该事件不会冒泡。
- `focusin`：元素节点将要获得焦点时触发，发生在`focus`事件之前。该事件会冒泡。
- `focusout`：元素节点将要失去焦点时触发，发生在`blur`事件之前。该事件会冒泡。

这四个事件都继承了`FocusEvent`接口。`FocusEvent`实例具有以下属性。

- `FocusEvent.target`：事件的目标节点。
- `FocusEvent.relatedTarget`：对于`focusin`事件，返回失去焦点的节点；对于`focusout`事件，返回将要接受焦点的节点；对于`focus`和`blur`事件，返回`null`。

由于`focus`和`blur`事件不会冒泡，只能在捕获阶段触发，所以`addEventListener`方法的第三个参数需要设为`true`。

    form.addEventListener('focus', function (event) {
      event.target.style.background = 'pink';
    }, true);

    form.addEventListener('blur', function (event) {
      event.target.style.background = '';
    }, true);

1
2
3
4
5
6
7

上面代码针对表单的文本输入框，接受焦点时设置背景色，失去焦点时去除背景色。

### # 7、CustomEvent 接口（自定义事件）

CustomEvent 接口用于生成自定义的事件实例。那些浏览器预定义的事件，虽然可以手动生成，但是往往不能在事件上绑定数据。如果需要在触发事件的同时，传入指定的数据，就可以使用 CustomEvent 接口生成的自定义事件对象。

浏览器原生提供`CustomEvent()`构造函数，用来生成 CustomEvent 事件实例。

    new CustomEvent(type, options)

1

`CustomEvent()`构造函数接受两个参数。第一个参数是字符串，表示事件的名字，这是必须的。第二个参数是事件的配置对象，这个参数是可选的。`CustomEvent`的配置对象除了接受 Event 事件的配置属性，只有一个自己的属性。

- `detail`：表示事件的附带数据，默认为`null`。

下面是一个例子。

    var event = new CustomEvent('build', { 'detail': 'hello' });

    function eventHandler(e) {
      console.log(e.detail);
    }

    document.body.addEventListener('build', function (e) {
      console.log(e.detail);
    });

    document.body.dispatchEvent(event);

1
2
3
4
5
6
7
8
9
10
11

上面代码中，我们手动定义了`build`事件。该事件触发后，会被监听到，从而输出该事件实例的`detail`属性（即字符串`hello`）。

下面是另一个例子。

    var myEvent = new CustomEvent('myevent', {
      detail: {
        foo: 'bar'
      },
      bubbles: true,
      cancelable: false
    });

    el.addEventListener('myevent', function (event) {
      console.log('Hello ' + event.detail.foo);
    });

    el.dispatchEvent(myEvent);

1
2
3
4
5
6
7
8
9
10
11
12
13

上面代码也说明，CustomEvent 的事件实例，除了具有 Event 接口的实例属性，还具有`detail`属性。

## # 十一、GlobalEventHandlers 接口 （全局事件处理接口）

指定事件的回调函数，推荐使用的方法是元素的`addEventListener`方法。

    div.addEventListener('click', clickHandler, false);

1

除了之外，还有一种方法可以直接指定事件的回调函数。

    div.onclick = clickHandler;

1

这个接口是由`GlobalEventHandlers`接口提供的。它的优点是使用比较方便，缺点是只能为每个事件指定一个回调函数，并且无法指定事件触发的阶段（捕获阶段还是冒泡阶段）。

`HTMLElement`、`Document`和`Window`都继承了这个接口，也就是说，各种 HTML 元素、`document`对象、`window`对象上面都可以使用`GlobalEventHandlers`接口提供的属性。下面就列出这个接口提供的主要的事件属性。

### # 1、GlobalEventHandlers.onabort （中断事件）

某个对象的`abort`事件（停止加载）发生时，就会调用`onabort`属性指定的回调函数。

各种元素的停止加载事件，到底如何触发，目前并没有统一的规定。因此实际上，这个属性现在一般只用在`<img>`元素上面。

    // HTML 代码如下
    // <img src="example.jpg" id="img">
    var img = document.getElementById('img');
    img.onabort = function () {
      console.log('图片加载中断');
    }

1
2
3
4
5
6

### # 2、GlobalEventHandlers.onerror （错误事件）

`error`事件发生时，就会调用`onerror`属性指定的回调函数。

`error`事件分成两种。

一种是 JavaScript 的运行时错误，这会传到`window`对象，导致`window.onerror()`。

    window.onerror = function (message, source, lineno, colno, error) {
      // ...
    }

1
2
3

`window.onerror`的处理函数共接受五个参数，含义如下。

- message：错误信息字符串
- source：报错脚本的 URL
- lineno：报错的行号，是一个整数
- colno：报错的列号，是一个整数
- error： 错误对象

另一种是资源加载错误，比如`<img>`或`<script>`加载的资源出现加载错误。这时，Error 对象会传到对应的元素，导致该元素的`onerror`属性开始执行。

    element.onerror = function (event) {
      // ...
    }

1
2
3

注意，一般来说，资源的加载错误不会触发`window.onerror`。

### # 3、GlobalEventHandlers.onload（加载完成事件）、GlobalEventHandlers.onloadstart（开始加载事件）

元素完成加载时，会触发`load`事件，执行`onload()`。它的典型使用场景是`window`对象和`<img>`元素。对于`window`对象来说，只有页面的所有资源加载完成（包括图片、脚本、样式表、字体等所有外部资源），才会触发`load`事件。

对于`<img>`和`<video>`等元素，加载开始时还会触发`loadstart`事件，导致执行`onloadstart`。

### # 4、GlobalEventHandlers.onfocus（获取焦点事件），GlobalEventHandlers.onblur（失去焦点事件）

当前元素获得焦点时，会触发`element.onfocus`；失去焦点时，会触发`element.onblur`。

    element.onfocus = function () {
      console.log("onfocus event detected!");
    };
    element.onblur = function () {
      console.log("onblur event detected!");
    };

1
2
3
4
5
6

注意，如果不是可以接受用户输入的元素，要触发`onfocus`，该元素必须有`tabindex`属性。

### # 5、GlobalEventHandlers.onscroll（滚动事件）

页面或元素滚动时，会触发`scroll`事件，导致执行`onscroll()`。

### # 6、GlobalEventHandlers.oncontextmenu（右键菜单事件），GlobalEventHandlers.onshow（显示右键菜单时触发）

用户在页面上按下鼠标的右键，会触发`contextmenu`事件，导致执行`oncontextmenu()`。如果该属性执行后返回`false`，就等于禁止了右键菜单。`document.oncontextmenu`与`window.oncontextmenu`效果一样。

    document.oncontextmenu = function () {// 禁用右键菜单
      return false;
    };

1
2
3

上面代码中，`oncontextmenu`属性执行后返回`false`，右键菜单就不会出现。

元素的右键菜单显示时，会触发该元素的`onshow`监听函数。

### # 7、其他的事件属性

鼠标的事件属性。

- onclick
- ondblclick
- onmousedown
- onmouseenter
- onmouseleave
- onmousemove
- onmouseout
- onmouseover
- onmouseup
- onwheel

键盘的事件属性。

- onkeydown
- onkeypress
- onkeyup

焦点的事件属性。

- onblur
- onfocus

表单的事件属性。

- oninput
- onchange
- onsubmit
- onreset
- oninvalid
- onselect

触摸的事件属性。

- ontouchcancel
- ontouchend
- ontouchmove
- ontouchstart

拖动的事件属性分成两类：一类与被拖动元素相关，另一类与接收被拖动元素的容器元素相关。

被拖动元素的事件属性。

- ondragstart：拖动开始
- ondrag：拖动过程中，每隔几百毫秒触发一次
- ondragend：拖动结束

接收被拖动元素的容器元素的事件属性。

- ondragenter：被拖动元素进入容器元素。
- ondragleave：被拖动元素离开容器元素。
- ondragover：被拖动元素在容器元素上方，每隔几百毫秒触发一次。
- ondrop：松开鼠标后，被拖动元素放入容器元素。

`<dialog>`对话框元素的事件属性。

- oncancel
- onclose

## # 文档

学习文档：https://wangdoc.com/javascript/
(opens new window)

编辑
(opens new window)
#JavaScript
上次更新: 2022/12/14, 19:36:42
DOM
浏览器模型

← DOM 浏览器模型→

最近更新

  01
Git修改分支名
08-11

  02
CSS给table的tbody添加滚动条
06-29

  03
我做了一个手写春联小网页，祝大家虎年暴富 原创
01-28

更多文章>

Copyright © 2016-2022 Hesb | MIT License

- 跟随系统
- 浅色模式
- 深色模式
- 阅读模式