JavaScript 中的事件

JavaScript 中的事件

所谓事件,说的就是用户或浏览器自身执行的某种动作,诸如 clickloadmouseover 等,事件处理程序响应某个事件的函数就叫事件处理程序(或事件侦听器)

HTML 事件处理程序

1
<input type="button" value="click me" onclick="alert(1)">

HTML 中指定事件处理程序有个缺点,即 HTMLJavaScript 代码紧密耦合,如果要更换处理程序,就要改动两个地方 HTML 代码和 JavaScript 代码

DOM 0 级事件处理程序

1
2
3
btn.onclick = function () { 
// ...
}

以这种方式添加的事件处理程序会在事件流的『冒泡阶段』被处理

1
btn.onclick = null // 删除事件处理程序

DOM 2 级事件处理程序

定义了两个方法 addEventListener()removeEventListener(),它们接收三个参数,依次为要处理的事件名,做为事件处理程序的函数,布尔值(true 表示捕获阶段,false 表示冒泡阶段)

1
2
3
btn.addEventListener('click', 'show', false)

btn.removeEventListener('click', 'show', false)

普通添加事件的方法不支持添加多个事件,最下面的事件会覆盖上面的,而事件绑定(addEventListener)方式添加事件可以添加多个,几个需要注意的地方

  • eventName 的值均不含 on,例如注册鼠标点击事件 eventNameclick
  • 处理函数中的 this 依然指的是指当前 DOM 元素
  • 通过 addEventListener 添加的事件处理程序,只能通过 removeEventListener 来删除(也就是说 addEventListener 添加的匿名函数将无法被删除)

IE 事件处理程序

1
2
3
attachEvent() // 添加事件

detachEvent() // 添加事件

接收相同的两个参数,事件处理程序的名称和事件处理程序的函数,不使用第三个参数的原因是 IE8 及更早版本只支持冒泡型事件,所以 attachEvent 添加的事件都会被添加到冒泡阶段

1
2
3
btn.attachEvent('onclick', show)

btn.detachEvent('onclick', show)

注意,通过 attachEvent 添加的事件第一个参数是 onclick 而非标准事件中的 click,它和 DOM 0 级事件处理程序的主要区别在于事件处理程序的『作用域』

采用 DOM 0 级处理方式,事件处理程序会在其所属元素的作用域内运行,而使用 attachEvent,事件处理程序会在全局作用域内运行,因此 this 等于 window

1
2
3
4
5
6
var btn = document.getElementById('btn')

btn.attachEvent('onclick', function () {
// 此处 this 是 window
alert(this)
})

跨浏览器的事件处理程序

事件对象 在触发 DOM 上的事件的时候都会产生一个对象,也就是事件对象 eventDOM 中的事件对象有以下一些比较常用的属性和方法

  • type 属性,用于获取事件的类型
  • target 属性,用于获取事件目标
  • stopPropagation() 方法,用于阻止事件冒泡
  • preventDefault() 方法,阻止事件的默认行为

兼容方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
var eventHandle = {
// 添加
addEvent: function (el, type, fn) {
if (el.addEventListener) {
el.addEventListener(type, fn, false)
} else if (el.attachEvent) {
el.attachEvent('on' + type, fn)
}
},
// 删除
removeEvent: function (el, type, fn) {
if (el.removeEventListener) {
el.removeEventListener(type, fn, false)
} else if (el.detachEvent) {
el.detachEvent('on' + type, fn)
}
},
// 事件对象
getEvent: function (e) {
return e ? event : window.event
},
// 事件监听的元素
getElement: function (e) {
return e.target || e.srcElement
},
// 阻止冒泡
stopBubble: function (e) {
if (e && e.stopPropagation) {
e.stopPropagation()
} else {
window.event.cancelBubble = true
}
},
// 阻止默认行为
stopDefault: function (e) {
if (e && e.preventDefault) {
e.preventDefault()
} else {
window.event.returnValue = false
}
return false
}
}

关于 event

  • event 代表事件的状态,例如触发 event 对象的元素、鼠标的位置及状态、按下的键等等
  • event 对象只在事件发生的过程中才有效

firefox 里的 eventIE 里的不同,IE 里的是全局变量,随时可用,firefox 里的要用参数引导才能用,是运行时的临时变量,在 IE/Opera 中是 window.event,在 Firefox 中是 event,而事件的对象,在 IE 中是 window.event.srcElement,在 Firefox 中是 event.targetOpera 中两者都可用,比如下面两句效果是相同的

1
2
3
4
5
function a(e) {
var e = e ? evt : ((window.event) ? window.event : null)
// firefox 下 window.event 为 null, IE 下 event 为 null
var e = e || window.event
}

jQuery 当中阻止事件冒泡的方法如下

1
2
3
4
5
// 阻止事件冒泡
e.stopPropagation()

// 阻止事件默认行为
e.preventDefault()

return false 等效于同时调用 e.preventDefault()e.stopPropagation()

事件委托

使用事件委托技术能让你避免对特定的每个节点添加事件监听器,事件监听器是被添加到它们的父元素上,事件监听器会分析从子元素冒泡上来的事件,找到是哪个子元素的事件,也就是利用冒泡的原理,把事件加到父级上,触发执行效果,可以提高性能,来看下面这个示例,我们需要在鼠标移入的过程当中触发每个 li 来改变它们的背景颜色

1
2
3
4
5
<ul >
<li>111111</li>
<li>222222</li>
<li>333333</li>
</ul>
1
2
3
4
5
6
7
8
for(var i = 0; i < li.length; i++) {
li[i].onmouseover = function () {
this.style.background = 'red'
}
li[i].onmouseout = function () {
this.style.background = ''
}
}

利用循环可以达到我们的目的,但是如果说我们可能有很多个 lifor 循环的话就比较影响性能了(譬如有几十上百个 li),所以在这种情况下,我们可以尝试使用用事件委托的方式来进行实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ul.onmouseover = function (ev) {
var ev = ev || window.event
var target = ev.target || ev.srcElement

if (target.nodeName.toLowerCase() == 'li') {
target.style.background = 'red'
}
}

ul.onmouseout = function (ev) {
var ev = ev || window.event
var target = ev.target || ev.srcElement

if (target.nodeName.toLowerCase() == 'li') {
target.style.background = ''
}
}

还有另外一个好处,就是新添加的元素还会有之前的事件,比如我们要实现一个点击 btn 动态的添加 li 的效果,相比利用 for 循环来实现,利用事件委托机制也可以达成我们的目标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ul.onmouseover = function (ev) {
var ev = ev || window.event
var target = ev.target || ev.srcElement
if (target.nodeName.toLowerCase() == 'li') {
target.style.background = 'red'
}
}

ul.onmouseout = function (ev) {
var ev = ev || window.event
var target = ev.target || ev.srcElement
if (target.nodeName.toLowerCase() == 'li') {
target.style.background = ''
}
}

btn.onclick = function () {
iNow++
var li = document.createElement('li')
li.innerHTML = iNow
oUl.appendChild(li)
}

事件流,冒泡与捕获

所谓事件流,即为了描述事件的传播而规定的一个事件传播方向,分为两个阶段,事件捕获和事件冒泡,正常情况下,事件先从最外层的元素向内捕获,然后从最内层的元素往外层传播,事件的触发一定是按照事件流的顺序而来

DOM 0 级

1
2
3
4
5
6
7
btn.onclick = function () {
alert(1)
}

btn.onclick = function () {
alert(2)
}

只能监听冒泡阶段,如果给同一个对象,同一个事件名绑定多个监听,后面的会覆盖掉之前的(这里需要注意 this 指向的是触发事件的 DOM 元素),IE 6/7/8 中事件只能冒泡到 document,不能继续冒泡到 window 对象上

所以一般不能给 window 添加 click 事件

DOM 2 级

1
2
3
btn.addEventListener('click', function () {
// ...
}, false)

最后一个参数,true 表示捕获阶段,而 false 则表示为冒泡阶段,几个注意事项

  • 所有现代浏览器都支持事件冒泡,并且会将事件一直冒泡到 window 对象
  • 如果不是最内层的元素同时绑定有捕获和冒泡事件,改变事件绑定的先后顺序,不会影响执行结果,依然是先捕获后冒泡
  • 如果是最内层的元素同时绑定有捕获和冒泡事件,则哪个事件写在前面就先执行哪一个,不再区分捕获或冒泡
  • 可以对同一个元素绑定多个事件监听函数,彼此之间不会覆盖,按先后顺序执行
  • this 指向的是触发事件的元素(也就是事件传播到的这个元素)

一个比较完整的案例,页面布局如下

1
2
3
4
5
6
7
<div id='box1'>
<div id='box2'>
<div id='box3'>

</div>
</div>
</div>

测试相关代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
box2.onclick = function () {
alert('A')
}

box2.onclick = function () {
alert('B')
}

box2.addEventListener('click', function () {
alert('C')
}, false)

box2.addEventListener('click', function () {
alert('D')
}, false)

box2.addEventListener('click', function () {
alert('E')
}, true)

box2.addEventListener('click', function () {
alert('F')
}, true)

box3.addEventListener('click', function () {
alert('G')
}, false)

box3.addEventListener('click', function () {
alert('H')
}, true)

执行后的结果依次为 E ==> F ==> G ==> H ==> B ==> C ==> D

自定义事件

我们不仅可以分配事件处理程序,还可以从 JavaScript 生成事件,不仅可以生成出于自身目的而创建的全新事件,还可以生成例如 clickmousedown 等内建事件,内建事件类形成一个层次结构(hierarchy),类似于 DOM 元素类,根是内建的 Event 类,我们可以像下面这样来创建 Event 对象

1
event = new Event(name, options)
  • name,事件类型,表示所创建事件的名称,可以是像 click 这样的字符串,或者是我们自定义的类似 my-event 这样的参数
  • options,字典类型的参数,接受以下字段
    • bubbles,可选,布尔类型,默认值为 false,表示该事件是否冒泡
    • cancelable,可选,布尔类型,默认值为 false,表示该事件能否被取消,如果为 true,那么默认行为就会被阻止
    • composed,可选,布尔类型,默认值为 false,指示事件是否会在影子 DOM 根节点之外触发侦听器

下面我们来创建一个支持冒泡且不能被取消的 look 事件的示例

1
2
3
4
5
6
7
8
9
var myEvent = new Event('look', {
bubbles: true,
cancelable: false
})

document.dispatchEvent(myEvent)

// 事件可以在任何元素触发,不仅仅是 document
myDiv.dispatchEvent(myEvent)

但是这种方式存在一定的局限性,那就是无法传递参数,如果我们想要传递参数的话,则可以使用 CustomEvent,从技术上讲 CustomEventEvent 一样,除了一点不同,即在第二个参数(对象)中,我们可以为我们想要与事件一起传递的任何自定义信息添加一个附加的属性 detail,具体用法和上面的 Event 对象十分类似,语法如下

1
var myEvent = new CustomEvent(eventname, options)

其中 options 可以是

1
2
3
4
5
6
7
{
detail: {
...
},
bubbles: true, // 是否冒泡
cancelable: false // 是否取消默认事件
}

其中 detail 可以存放一些初始化的信息,可以在触发的时候调用,其他属性就是定义该事件是否具有冒泡等等功能,下面是一个简单的示例

1
2
3
4
5
6
7
8
9
el.addEventListener('hello', function(e) {
console.log(e.detail.name) // zhangsan
})

el.dispatchEvent(new CustomEvent('hello', {
detail: {
name: 'zhangsan'
}
}))

关于 CustomEvent 有一个坑就是,如果需要在事件处理函数(addEventListener)当中使用 event.preventDefault() 的话,则需要在 CustomEvent 当中指定 cancelable: true,否则 event.preventDefault() 调用将会被忽略,如下

1
2
3
4
5
6
7
8
el.dispatchEvent(new CustomEvent('hello', {
// 没有这个标志,preventDefault 将不起作用
cancelable: true
}))

el.addEventListener('hello', function (event) {
// 如果这里需要使用 event.preventDefault() 的话
})

另外还有一点关于 CustomEvent 需要注意的就是,事件中的事件是同步的,通常事件是在队列中处理的,也就是说如果浏览器正在处理 onclick,这时发生了一个新的事件,例如鼠标移动了,那么它会被排入队列,相应的 mousemove 处理程序将在 onclick 事件处理完成后被调用

值得注意的例外情况就是,一个事件是在另一个事件中发起的,例如使用 dispatchEvent,这类事件将会被立即处理,即在新的事件处理程序被调用之后,恢复到当前的事件处理程序,例如在下面的代码中,menu-open 事件是在 onclick 事件执行过程中被调用的,它会被立即执行,而不必等待 onclick 处理程序结束

1
2
3
4
5
6
7
8
9
10
button.onclick = function () {
alert(1)
button.dispatchEvent(new CustomEvent('menu-open', {
bubbles: true
}))
alert(2)
}

// 在 1 和 2 之间触发
document.addEventListener('menu-open', () => alert('nested'))

输出顺序为 1 ==> nested ==> 2,但是这里需要注意的是,其中的配置项 bubbles: true 是必须的,否则无法触发,如果想让 onclick 不受 menu-open 或者其它嵌套事件的影响,优先被处理完毕,那么我们就可以将 dispatchEvent 放在 onclick 末尾,或者将其包装到零延迟的 setTimeout

1
2
3
4
5
6
7
button.onclick = function () {
alert(1)
setTimeout(() => menu.dispatchEvent(new CustomEvent('menu-open', {
bubbles: true
})))
alert(2)
}

现在 dispatchEvent 在当前代码执行完成之后异步运行,包括 mouse.onclick,因此事件处理程序是完全独立的,输出顺序变成 1 ==> 2 ==> nested

总结

  • DOM 0 级添加到冒泡阶段
  • DOM 0 级同名事件会发生覆盖
  • true 表示捕获阶段,false 表示冒泡阶段,会先执行捕获
  • DOM 2 级同名事件不会覆盖,按先后顺序执行
  • DOM 2 级最内层的元素不区分冒泡和捕获,按先后顺序执行(无论是 DOM 0 级还是 DOM 2 级)
  • 通用的 Event(name, options) 构造器接受任意事件名称和一个字典类型的参数(bubbles/cancelable/composed
  • 对于自定义事件,我们应该使用 CustomEvent 构造器,它有一个名为 detail 的附加选项可以用来传递参数,然后所有处理程序可以以 event.detail 的形式来访问它

参考

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×