1. 问题
如果在没有敲代码实际运行的情况下,能肯定且正确的回答以下问题,则可以不继续往下看。如果对某个题的回答存在疑虑或不清楚怎么回事,可以选择继续阅读。
- 以下代码的输出顺序
<div id="div1" onclick="console.log(7)">
<div id="div2" onclick="console.log(6)">div>
div>
复制代码
const div1 = document.getElementById('div1')
const div2 = document.getElementById('div2')
div2.onclick = function () { console.log(5) }
div1.addEventListener('click', function () { console.log(1) })
div2.addEventListener('click', function () { console.log(2) }, false)
div1.addEventListener('click', function () { console.log(3) }, { capture: true })
div2.addEventListener('click', function () { console.log(4) }, { capture: true })
复制代码
- 什么是事件流,同一个事件有几个阶段?
- 如何移除通过HTML属性、element.onclick等注册的事件处理函数?
如果继续阅读的话,上面的问题,会在正文中有分析。
注:如无特殊说明,本文以chrome测试的结果为准
2. 事件流
当在页面上某个元素触发特定事件时,页面上哪些部分会触发该事件?现代浏览器开发者设定,除了被点击的目标元素,所有祖先元素都会触发该事件,一直到window(现代浏览器,IE4和网景是document)。
这样又出现了新的问题,在window和目标元素都触发事件,那是先在目标元素上触发呢,还是先在其他元素上触发呢?这就是事件流的概念。
事件流是事件在目标元素和祖先元素间的触发顺序,在早期,IE和网景实现了相反的事件流,IE4实现的是先触发目标元素的事件,再向上一层一层触发祖先元素的事件,到document对象(即事件冒泡);
而网景则是先document触发,然后再一层一层到达目标元素(即 事件捕获)。但现代浏览器实现的事件流是DOM2级的事件标准,包含了IE、网景的实现,而且都把window也包含在内即,div -> body -> html -> document -> window
。
DOM2级事件流标准有三个阶段:事件捕获阶段、出于目标阶段、事件冒泡阶段。先发生在事件捕获阶段,然后到目标元素,最后再冒泡上去到window。
既然在冒泡阶段和捕获阶段都会触发事件,那当添加了事件监听方法之后,是不是在每个元素上都每次事件都会触发两次呢?显然不是,在DOM2中事件监听机制提供了一个参数来决定事件是在捕获阶段生效还是在冒泡阶段生效,即addEventListener
// DOM2级事件监听方法
// useCapture(可选):Boolean,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
// 默认为false,即在冒泡阶段触发
target.addEventListener(type, listener[, useCapture]);
// 目前的事件监听方法
// options(可选): 一个指定有关 listener 属性的可选参数对象。可用的选项如下:
// capture: Boolean,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
// once: Boolean,表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除。
// passive: Boolean,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。
target.addEventListener(type, listener[, options]);
复制代码
在平常开发中,很少需要用到第三个参数,使用的是默认的false值,所以很多时候并没有弄清楚,第三个参数有没有、true、false有什么区别。 可以验证事件流的过程:
<div id="div1"> <div id="div2">div> div>
复制代码
const div1 = document.getElementById('div1')
const div2 = document.getElementById('div2')
// capture默认为false,在事件流到达目标之后再网上传递,比目标元素上的事件触发晚
div1.addEventListener('click', function () { console.log(1) })
div2.addEventListener('click', function () { console.log(2) }, false)
// capture为true,在到达目标元素前的捕获阶段在div1上触发,最先执行回调
div1.addEventListener('click', function () { console.log(3) }, { capture: true })
div2.addEventListener('click', function () { console.log(4) }, { capture: true })
复制代码
div2是目标元素,在它上面capture为true是否会先触发? 其实当事件流处于于目标阶段后,事件的回调函数会按照注册的顺序触发,而不管capture是false还是true,详情参考链接。
所以上例的输出顺序就比较明显了,3, 2, 4, 1
3. 事件监听、移除监听
事件监听的方式有三种:
-
通过HTML属性的方式
-
DOM0中可以通过js脚本来给指定元素提供事件处理函数,即
element.onclick = handlerhandler为匿名函数或指定的函数名 复制代码
-
在DOM2中,添加了新的事件监听API,即
addEventListener(type, handler[, options | useCapture])
,同时提供了取消监听的removeEventListener(type, handler[, options | useCapture])
;显然事件处理函数注册后,要取消监听,type/hanlder/useCapture的一致。
相比html属性方式、DOM0级监听方式,addEventListener的优势是什么呢?主要有以下几点:
-
addEventLinster可为同一个事件注册多个回调函数,以此触发。而DOM0级注册会覆盖
-
addEventLinster可以通过参数决定监听是在冒泡阶段生效,还是在捕获阶段生效。element.onclick注册的监听只会在冒泡阶段生效
-
更方便移除监听
沿用前面的例子:
<div id="div1">
<div id="div2" onclick="console.log(7)">div>
div>
复制代码
const div2 = document.getElementById('div2')
// 如果同时使用了HTML属性方式,和DOM0方式,则DOM0方式会覆盖HTML属性方式
div2.onclick = function () { console.log(8) }
// 同样这一个会覆盖掉上一条,只会log出9
div2.onclick = function () { console.log(9) }
div2.addEventLisnter('click', function () { console.log(10) })
div2.addEventLisnter('click', function () { console.log(11) }) // 不会覆盖,会log出9, 10, 11
复制代码
DOM2可以通过removeEventListener的方式移除处理函数,HTML属性方式注册和DOM0的监听如何移除呢?
div2.onclick = null
// or
div2.setAttributer('onclick', false)
复制代码
这两个方法都可以把HTML属性注册或element.onclick方式注册的监听移除,但不会影响addEventListener注册的监听。
到这里,最开始问题1中的输出顺序也容易了
<div id="div1" onclick="console.log(7)">
<div id="div2" onclick="console.log(6)">div>
div>
复制代码
const div1 = document.getElementById('div1')
const div2 = document.getElementById('div2')
div2.onclick = function () { // 覆盖掉html中的onclick属性,6不会被输出,同时在冒泡阶段,这个方法最先被注册
console.log(5)
}
div1.addEventListener('click', function () { console.log(1) })
div2.addEventListener('click', function () { console.log(2) }, false)
div1.addEventListener('click', function () { console.log(3) }, { capture: true })
div2.addEventListener('click', function () { console.log(4) }, { capture: true })
复制代码
结果是: 3, 5, 2, 4, 7, 1,这里需要注意的是5先于2、4被log出来,而7先于1被log出来(在chrome中的结果)
参考:
- MDN - EventTarget.addEventListener()
- javascript高级程序设计3版, 13章 - 事件