跳至主要內容

事件

Mr.Chen《JavaScript教程》笔记JavaScript大约 88 分钟约 26333 字

事件

一、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('触发事件')"></div>
</body>

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

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

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

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

一旦指定的事件发生,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方法返回一个布尔值,表示有没有按下特定的功能键。它的参数是一个表示功能键open in new window的字符串。

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属性返回一个字符串,表示当前按下的键的字符串形式。该属性只读

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

  • 数字键 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 浏览器的返回值如下。完整列表可以参考文档open in 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

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>
</div>

上面代码中,如果不取消拖拉事件或者阻止默认行为,就不能在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事件的触发频率,限制在一秒一次。

lodashopen in new window函数库提供了现成的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/open in new window

上次编辑于: