彻底搞懂JS的事件原理

简介: 系列

事件

一、EventTarget 接口

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

1、概述

DOM 的事件操作(监听和触发),都定义在EventTarget接口。所有节点对象都部署了这个接口,其他一些需要事件通信的浏览器内置对象(比如,XMLHttpRequestAudioNodeAudioContext)也部署了这个接口。

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

  • addEventListener:绑定事件的监听函数
  • removeEventListener:移除事件的监听函数
  • dispatchEvent:触发事件

2、EventTarget.addEventListener()

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

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

该方法接受三个参数。

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

下面是一个例子。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3、EventTarget.removeEventListener()

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

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

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

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

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

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

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

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

4、EventTarget.dispatchEvent()

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

target.dispatchEvent(event)

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

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

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

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

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

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

二、事件模型

1、监听函数

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

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

1.2 HTML 的 on- 属性

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1.3 EventTarget.addEventListener()

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

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

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

1.4 小结

上面三种方法,

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

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

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

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

2、this 的指向 (指向元素节点)

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

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

执行上面代码,点击后会输出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
);

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

3、事件的传播(捕获、目标、冒泡)

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

  • 第一阶段:从window对象传导到目标节点(上层传到底层),称为“捕获阶段”(capture phase)。
  • 第二阶段:在目标节点上触发,称为“目标阶段”(target phase)。
  • 第三阶段:从目标节点传导回window对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。

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

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

上面代码中, <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' 冒泡阶段

上面代码表示,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,接着依次是documenthtmldocument.documentElement)和bodydocument.body)。也就是说,上例的事件传播顺序,在捕获阶段依次为windowdocumenthtmlbodydivp,在冒泡阶段依次为pdivbodyhtmldocumentwindow

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

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

4、事件的代理

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

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

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

上面代码中,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表示在冒泡阶段(默认值)绑定事件监听函数

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

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

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

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

上面代码中,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);
});

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

三、Event 对象

1. 概述

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

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

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

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

  • bubbles:布尔值,可选,默认为false,表示事件对象是否冒泡
  • cancelable:布尔值,可选,默认为false,表示事件是否可以被取消,即能否用Event.preventDefault()取消这个事件。一旦事件被取消,就好像从来没有发生过,不会触发浏览器对该事件的默认行为。
var ev = new Event( // 创建一个新的事件实例
  'look', // 事件名称
  { // 事件配置
    'bubbles': true, // 事件是否冒泡
    'cancelable': false // 事件是否可以被取消
  }
);
document.dispatchEvent(ev); // 触发ev实例,该实例是look事件

上面代码新建一个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事件

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

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

div.dispatchEvent(click);

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

2. 实例属性

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

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

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

var phase = event.eventPhase;

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 默认不可取消

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

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

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

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

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

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

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

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

事件传播过程中,不同节点的监听函数内部的Event.targetEvent.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);

上面代码中, <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"

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

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

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

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

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

例子:计算鼠标移动速度
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;
});

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

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

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

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

2.7 Event.detail 事件的细节(单击or双击等)

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

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

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

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
);

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

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

例子:只能输入字母的输入框
// 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();
  }
}

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

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

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

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

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

上面代码中,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);

上面代码在el节点上,为click事件添加了两个监听函数l1l2。由于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]

上面代码中,click事件的最底层节点是p,向上依次是divbodyhtmldocumentWindow

四、鼠标事件

1、鼠标事件的种类

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

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

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

dblclick事件则会在mousedownmouseupclick之后触发。

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);

上面代码中,在父节点内部进入子节点,不会触发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);

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

2、MouseEvent 接口概述

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

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

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

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

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:节点对象,表示事件的相关节点,默认为nullmouseentermouseover事件时,表示鼠标刚刚离开的那个元素节点;mouseoutmouseleave事件时,表示鼠标正在进入的那个元素节点。

下面是一个例子。

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

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

3、MouseEvent 接口的实例属性

3.1 MouseEvent.altKey,MouseEvent.ctrlKey,MouseEvent.metaKey,MouseEvent.shiftKey

MouseEvent.altKeyMouseEvent.ctrlKeyMouseEvent.metaKeyMouseEvent.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);
}

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

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

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)

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'
  );
}

这两个属性还分别有一个别名MouseEvent.xMouseEvent.y

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

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

currentEvent.movementX = currentEvent.screenX - previousEvent.screenX

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

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

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

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'
  );
}

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
);

上面代码中,鼠标如果在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
);

上面代码中,页面高度为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

4、MouseEvent 接口的实例方法

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

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

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

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

5、WheelEvent 接口 (滚轮)

5.1 概述

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

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

var wheelEvent = new WheelEvent(type, options);

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

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

5.2 实例属性

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

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

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

五、键盘事件

1、键盘事件的种类

键盘事件由用户击打键盘触发,主要有keydownkeypresskeyup三个事件,它们都继承了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) // 参数一,事件类型;参数二,事件配置对象

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);

3.2 KeyboardEvent.code 键码

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

下面是一些常用键的字符串形式,其他键请查文档

  • 数字键0 - 9:返回digital0 - digital9
  • 字母键A - z:返回KeyA - KeyZ
  • 功能键F1 - F12:返回 F1 - F12
  • 方向键:返回ArrowDownArrowUpArrowLeftArrowRight
  • Alt 键:返回AltLeftAltRight
  • Shift 键:返回ShiftLeftShiftRight
  • Ctrl 键:返回ControlLeftControlRight

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返回一个布尔值,代表该键是否被按着不放,以便判断是否重复这个键,即浏览器会持续触发keydownkeypress事件,直到用户松开手为止。

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

上面代码表示,只要ControlAltMeta里面,同时按下任意两个或两个以上的键就返回。

六、进度事件

1、进度事件的种类

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

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

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

下面是一个例子。

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

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

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

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

function loaded() {
  // ...
}

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

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

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

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

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

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

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

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

2、ProgressEvent 接口

2.1 概述

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

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

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

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

  • lengthComputable:布尔值,表示加载的总量是否可以计算,默认是false
  • loaded:整数,表示已经加载的量,默认是0
  • total:整数,表示需要加载的总量,默认是0

2.2 ProgressEvent的实例属性。

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

如果ProgressEvent.lengthComputablefalseProgressEvent.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%

上面代码先构造一个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('用户取消了传输');
}

上面是下载过程的进度事件,还存在上传过程的进度事件。这时所有监听函数都要放在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、表单事件的种类

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);

上面代码中,改变下拉框选项时,会触发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);

选中的文本可以通过event.target元素的selectionDirectionselectionEndselectionStartvalue属性拿到。

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

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

1.4 invalid 事件 (表单提交不满足条件触发)

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

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

上面代码中,输入框是必填的。如果不填,用户点击按钮提交时,就会触发输入框的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>

2、InputEvent 接口(input事件的实例)

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

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

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

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

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

(2)InputEvent.inputType 变更类型

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

对于常见情况,Chrome 浏览器的返回值如下。完整列表可以参考文档

  • 手动插入文本: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

2、Touch 接口

2.1 Touch 接口概述 (单个触摸点)

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

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

var touch = new Touch(touchOptions);

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:可选,类型为数值,范围在01之间,表示触摸压力。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);
(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属性表示触摸区域的椭圆的旋转角度,单位为度数,在090度之间。

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

下面是一个示例。

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)';
};
(4)Touch.force 触摸压力

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

(5)Touch.target 开始触摸时的元素

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

3、TouchList 接口

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

它的实例主要通过触摸事件的TouchEvent.touchesTouchEvent.changedTouchesTouchEvent.targetTouches这几个属性获取。

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

  • TouchList.length:数值,表示成员数量(即触摸点的数量)。
  • TouchList.item():返回指定位置的成员,它的参数是该成员的位置编号(从零开始)。

4、TouchEvent 接口

4.1 概述

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

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

new TouchEvent(type, options)

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

  • touchesTouchList实例,代表所有的当前处于活跃状态的触摸点,默认值是一个空数组[]
  • targetTouchesTouchList实例,代表所有处在触摸的目标元素节点内部、且仍然处于活动状态的触摸点,默认值是一个空数组[]
  • changedTouchesTouchList实例,代表本次触摸事件的相关触摸点,默认值是一个空数组[]
  • 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);
(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);
(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);
(4)TouchEvent.targetTouches (目标元素内活动的触摸点集合)

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

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

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

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、拖拉事件的种类

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

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

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

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);

上面代码中,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>

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

/* 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);

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

  • 拖拉过程只触发以上这些拖拉事件,尽管鼠标在移动,但是鼠标事件不会触发
  • 将文件从操作系统拖拉进浏览器,不会触发dragstartdragend事件。
  • dragenterdragover事件的监听函数,用来取出拖拉的数据(即允许放下被拖拉的元素)。由于网页的大部分区域不适合作为放下拖拉元素的目标节点,所以这两个事件的默认设置为当前节点不允许接受被拖拉的元素。如果想要在目标节点上放下的数据,首先必须阻止这两个事件的默认行为
<div ondragover="return false">
<div ondragover="event.preventDefault()">

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

2、DragEvent 接口

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

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

new DragEvent(type, options)

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

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

3、DataTransfer 接口概述

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

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

var dataTrans = new DataTransfer();

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

拖拉的数据分成两方面:数据的种类(又称格式)和数据的值。数据的种类是一个 MIME 字符串(比如text/plainimage/jpeg),数据的值是一个字符串。一般来说,如果拖拉一段文本,则数据默认就是那段文本;如果拖拉一个链接,则数据默认就是链接的 URL。

拖拉事件开始时,开发者可以提供数据类型和数据值。拖拉过程中,开发者通过dragenterdragover事件的监听函数,检查数据类型,以确定是否允许放下(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';
});

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

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

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

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

  • copy:复制被拖拉的节点
  • move:移动被拖拉的节点
  • link:创建指向被拖拉节点的链接
  • copyLink:允许copylink
  • copyMove:允许copymove
  • linkMove:允许linkmove
  • all:允许所有效果
  • none:无法放下被拖拉的节点
  • uninitialized:默认值,等同于all

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

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

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

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

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

只要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);

上面代码中,通过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);
  }
});

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();
}

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

4.5 DataTransfer.items

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

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

  • length:返回成员的数量
  • add(data, type):增加一个指定内容和类型(比如text/htmltext/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);
    }
  }
});

5、DataTransfer 的实例方法

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

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

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

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

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

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

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

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

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

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

上面代码中,拖拉数据实际上是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);

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

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');

上面代码中,通过在同一个事件上面,存放三种类型的数据,使得拖拉事件可以在不同的对象上面,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();
}

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

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();
}

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

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

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

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

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();
}

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

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

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

上面代码清除事件所带的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、资源事件

1.1 beforeunload 事件 (关闭窗口前调用)

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

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

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

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

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

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

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

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

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

1.2 unload 事件 (即将关闭窗口时调用)

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

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

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

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

1.3 load 事件,error 事件 ,abort事件 (页面或某个资源加载成功/失败/取消时调用)

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

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

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

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

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

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}

上面代码中,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

3、网页状态事件

3.1 DOMContentLoaded 事件 (DOM内容加载完成后触发)

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

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

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

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

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

3.2 readystatechange 事件(Document 对象和 XMLHttpRequest 对象的readyState属性发生变化时触发)

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

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

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

4、窗口事件

4.1 scroll 事件 (文档或文档元素滚动时触发)

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

window.addEventListener('scroll', callback);

该事件会连续地大量触发,所以它的监听函数之中不应该有非常耗费计算的操作。推荐的做法是使用requestAnimationFramesetTimeout控制该事件的触发频率,然后可以结合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!');
});

上面代码中,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() {
    // ...
  }
}());

上面代码中,每次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));

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

lodash函数库提供了现成的throttle函数,可以直接使用。

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

本书前面介绍过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);

该事件也会连续地大量触发,所以最好像上面的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(); // 设置元素为全屏展示
});

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();
});

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

6、焦点事件

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

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

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

  • FocusEvent.target:事件的目标节点。
  • FocusEvent.relatedTarget:对于focusin事件,返回失去焦点的节点;对于focusout事件,返回将要接受焦点的节点;对于focusblur事件,返回null

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

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

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

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

7、CustomEvent 接口(自定义事件)

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

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

new CustomEvent(type, options)

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);

上面代码中,我们手动定义了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);

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

十一、GlobalEventHandlers 接口 (全局事件处理接口)

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

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

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

div.onclick = clickHandler;

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

HTMLElementDocumentWindow都继承了这个接口,也就是说,各种 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('图片加载中断');
}

2、GlobalEventHandlers.onerror (错误事件)

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

error事件分成两种

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

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

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

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

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

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

注意,一般来说,资源的加载错误不会触发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!");
};

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

5、GlobalEventHandlers.onscroll(滚动事件)

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

6、GlobalEventHandlers.oncontextmenu(右键菜单事件),GlobalEventHandlers.onshow(显示右键菜单时触发)

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

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

上面代码中,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/

相关文章
|
3月前
|
存储 前端开发 JavaScript
【面试题】前端 js那些需要注意的小细节
【面试题】前端 js那些需要注意的小细节
|
12月前
|
JavaScript 前端开发 Java
都2022年了你不会还没搞懂js中的事件吧
最近在面试时候,发现很多小伙伴对js事件还不太熟悉,比如绑定事件的方式、事件监听、事件委托、事件传播、事件捕获、事件冒泡等等。今天笔者总结下,希望可以帮助到你们。
56 0
|
Web App开发 缓存 监控
|
JavaScript
js基础笔记学习221事件2
js基础笔记学习221事件2
67 0
js基础笔记学习221事件2
|
JavaScript
js基础笔记学习220事件1
js基础笔记学习220事件1
40 0
js基础笔记学习220事件1
|
JavaScript
js基础笔记学习313事件的处理
js基础笔记学习313事件的处理
70 0
js基础笔记学习313事件的处理
|
JavaScript
js基础笔记学习314事件的处理2
js基础笔记学习314事件的处理2
35 0
js基础笔记学习314事件的处理2
|
JavaScript
js基础笔记学习244事件对象
js基础笔记学习244事件对象
60 0
js基础笔记学习244事件对象
|
JavaScript
js基础笔记学习265定时器1
js基础笔记学习265定时器1
37 0
js基础笔记学习265定时器1
|
JavaScript 前端开发
JS的精髓,事件详解
鼠标单击 单击事件onclick,如点击某个按钮弹出一个提示框。这里要特别注意一点,单击事件不只是按钮才有,任何元素我们都可以为它添加单击事件!
136 0
JS的精髓,事件详解