Cruyun's Blog


Talk is cheap, show you my code


事件机制

事件监听

事件监听有以下三种方法。

  1. HTML内联属性
    类似<button onclick="alert('你点击了这个按钮');">点击这个按钮</button>的方式,这种方式会使 JS 与 HTML 高度耦合,不利于开发和维护,不推荐使用。

  2. DOM属性绑定(事件处理器属性)
    使用DOM元素的onXXX属性设置,简单易懂,兼容性好。确定是只能绑定一个处理函数。

1
2
var btn = document.querySelector('button');
btn.onclick = function() {...}

一些事件非常通用,几乎在任何地方都可以用(比如 onclick 几乎可以用在几乎每一个元素上),然而另一些元素就只能在特定场景下使用,比如我们只能在 video 元素上使用 onplay 。

  1. 事件监听器

使用事件监听函数 element.addEventListener(<event-name>, <callback>, <use-capture>);,在 element 这个对象上面添加一个事件监听器,当监听到有 事件发生的时候,调用 这个回调函数。至于 这个参数,表示该事件监听是在“捕获”阶段中监听(设置为 true)还是在“冒泡”阶段中监听(设置为 false)。 如果没有指定, useCapture 默认为 false 。

注意: 对于事件目标上的事件监听器来说,事件会处于“目标阶段”,而不是冒泡阶段或者捕获阶段。在目标阶段的事件会触发该元素(即事件目标)上的所有监听器,而不在乎这个监听器到底在注册时useCapture 参数值是true还是false。

1
2
3
4
5
6
7
8
var btn = document.querySelector('button');
function bgChange() {
var rndCol = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
document.body.style.backgroundColor = rndCol;
}
btn.addEventListener('click', bgChange);

removeEventListener()方法移除事件监听器。例如btn.removeEventListener('click', bgChange);

另外,给同一个监听器注册多个处理器,如果使用事件处理器属性,后面的处理器会覆盖前面的。

1
2
myElement.onclick = functionA;
myElement.onclick = functionB;

而下面这种方法可以正常工作:

1
2
myElement.addEventListener('click', functionA);
myElement.addEventListener('click', functionB);

IE9之前的版本中无法使用标准的 addEventListener 函数,而是使用自家的 attachEvent,具体用法:element.attachEvent(, );

它只支持监听在冒泡阶段触发的事件,所以为了统一,在使用标准事件监听函数的时候,第三参数传递 false。

事件触发

  1. 事件捕获阶段: window 往事件触发处传播,遇到注册的捕获事件会触发
  2. 处于目标阶段: 传播到事件触发处时触发注册的事件
  3. 事件冒泡阶段: 从事件触发处往 window 传播,遇到注册的冒泡事件会触发

不同的浏览器对此有着不同的实现,IE10及以下不支持捕获型事件,所以就少了一个事件捕获阶段,IE11、Chrome 、Firefox、Safari等浏览器则同时存在

事件触发一般来说会按照上面的顺序进行,但是也有特例,如果给一个目标节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行。

1
2
3
4
5
6
7
// 以下会先打印冒泡然后是捕获
node.addEventListener('click',(event) =>{
console.log('冒泡')
},false);
node.addEventListener('click',(event) =>{
console.log('捕获 ')
},true)

事件捕获 vs 事件冒泡

事件冒泡和事件捕获分别由微软和网景公司提出,这两个概念都是为了解决页面中事件流(事件发生顺序)的问题。

在现代浏览器中,默认情况下,所有事件处理程序都在冒泡阶段进行注册
当一个事件发生在具有父元素的元素上时,现代浏览器运行两个不同的阶段 - 捕获阶段和冒泡阶段。

在捕获阶段:

浏览器检查元素的最外层祖先,是否在捕获阶段中注册了一个事件处理程序,如果是,则运行它。 然后,它移动到最外层祖先中的下一个元素,并执行相同的操作,然后是下一个元素,依此类推,直到到达实际点击的元素。

在冒泡阶段,恰恰相反:

浏览器检查实际点击的元素是否在冒泡阶段中注册了一个事件处理程序,如果是,则运行它 然后它移动到下一个直接的祖先元素,并做同样的事情,然后是下一个,等等,直到它到达最外层祖先元素。

例子:

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
<div id="s1">s1
<div id="s2">s2</div>
</div>
<script>
s1.addEventListener("click",function(e){
console.log("s1 冒泡事件"); },false);
s2.addEventListener("click",function(e){
console.log("s2 冒泡事件");},false);
s1.addEventListener("click",function(e){
console.log("s1 捕获事件");},true);
s2.addEventListener("click",function(e){
console.log("s2 捕获事件");},true);
</script>
/** 点击 s1
s1 冒泡事件
s1 捕获事件
**/
/** 点击s2
s1 捕获事件
s2 冒泡事件
s2 捕获事件
s1 冒泡事件
**/

点击 s2,click 事件从 document -> html -> body -> s1 -> s2 捕获前进,输出“s1 捕获事件”后到目的结点 s2,s2上先后注册了冒泡和捕获,于是按注册顺序输出 “ s2 冒泡事件” 、“s2 捕获事件”。下面进入冒泡阶段,从s2 -> s1 -> body -> html -> document 冒泡前进,在s1发现冒泡事件,于是输出“s1 冒泡事件”

事件对象

有时候在事件处理函数内部,您可能会看到一个固定指定名称的参数,例如event,evt或简单的e。 这被称为事件对象,它被自动传递给事件处理函数,以提供额外的功能和信息。

事件对象会作为第一个参数,传递给我们的回调函数。我们可以使用下面代码,在浏览器中打印出这个事件对象:

1
2
3
4
var btn = document.getElementsByTagName('button');
btn[0].addEventListener('click', function(event) {
console.log(event);
}, false);

比较常用的几个属性和方法:

  • type(string):事件的名称,比如 “click”。
  • target(node):事件要触发的目标节点。
  • currentTarget(node): 它就指向正在处理事件的元素:这恰是我们需要的。很不幸的是微软模型中并没有相似的属性, 你也可以使用”this”关键字。事件属性也提供了一个值可供访问:event.currentTarget。
  • bubbles (boolean)::表明该事件是否是在冒泡阶段触发的。该属性为只读属性。
1
2
3
4
5
6
7
function goInput(e) {
if (!e.bubbles) {
passItOn(e);
} else {
doOutput(e);
}
}
  • preventDefault (function):这个方法可以禁止一切默认的行为,例如点击 a 标签时,会打开一个新页面,如果为 a 标签监听事件 click 同时调用该方法,则不会打开新页面。
  • cancelable (boolean): 这个属性表明该事件是否可以通过调用 event.preventDefault 方法来禁用默认行为。
  • eventPhase (number):这个属性的数字表示当前事件触发在什么阶段。
    0: 事件目前没有发生
    1: 事件目前处于捕获阶段
    2: 事件到达目标节点
    3: 事件处于冒泡阶段
  • pageX 和 pageY (number): 这两个属性表示触发事件时,鼠标相对于页面的坐标。
  • isTrusted (boolean): 表明该事件是浏览器触发(用户真实操作触发),还是 JavaScript 代码触发的。

事件代理

事件绑定后,检测顺序就会从被绑定的DOM下滑到触发的元素,再冒泡会绑定的DOM上。也就是说,如果你监听了一个DOM节点,那也就等于你监听了其所有的后代节点。

代理的意思就是只监听父节点的事件触发,以来代理对其后代节点的监听,而你需要做的只是通过currentTarget属性得到触发元素并作出回应。

使用事件代理意味着你可以节省大量重复的事件监听,以减少浏览器资源消耗。还有一个好处就是让HTML独立起来,比如之后还有要加子元素的需求,也不需要再为其单独加事件监听了

下面举个例子 :

1
2
3
4
5
6
7
8
<ul id="color-list">
<li>red</li>
<li>yellow</li>
<li>blue</li>
<li>green</li>
<li>black</li>
<li>white</li>
</ul>

如果点击页面中的li元素,然后输出li当中的颜色,我们通常会这样写:

1
2
3
4
5
6
7
8
9
10
(function(){
var color_list = document.getElementById('color-list');
var colors = color_list.getElementsByTagName('li');
for(var i=0;i<colors.length;i++){
colors[i].addEventListener('click',showColor,false); };
function showColor(e){
var x = e.target;
console.log("The color is " + x.innerHTML);
};
})();

利用事件流的特性,我们只绑定一个事件处理函数也可以完成:

1
2
3
4
5
6
7
8
9
10
(function(){
var color_list =document.getElementById('color-list');
color_list.addEventListener('click',showColor,false);
function showColor(e){
var x = e.target;
if(x.nodeName.toLowerCase() === 'li'){
console.log('The color is ' + x.innerHTML);
}
}
})();

阻止默认行为:stopPropagation (function)、stopImmediatePropagation

  • stopPropagation (function): 很多时候,我们触发某个元素,会顺带触发出它父级身上的事件,这有时候是我们不想要的,大多数我们想要的还是事件相互独立。所以我们可以选择阻止事件冒泡,使用event.stopPropagation().
1
2
3
4
5
6
7
8
9
element.addEventListener('click', function (event) {
event.stopPropagation();
console.log(1);
});
element.addEventListener('click', function(event) {
// 会触发
console.log(2);
});
  • stopImmediatePropagation (function): 与 stopPropagation 类似,就是阻止触发其他监听函数。但是与 stopPropagation 不同的是,它更加 “强力”,阻止除了目标之外的事件触发,甚至阻止针对同一个目标节点的相同事件。
1
2
3
4
5
6
7
8
9
10
11
12
element.addEventListener('click', function (event) {
// 会触发
console.log(‘改方法内的可以执行’);
event.stopImmediatePropagation();
// 会触发
console.log(1);
});
element.addEventListener('click', function(event) {
// 不会被触发
console.log(2);
});

参考: