0%

javascript-event-model

JavaScript事件模型

在JavaScript中,有两种事件的传递方向,一种是由内层元素向外层元素传递,也叫自底向上的方式,称作事件冒泡,好比水中的气泡由水底向水面上升的过程。另一种叫做事件捕获,方向刚好相反,从外层元素向内层元素传递,也叫自顶向下。

目前主流的浏览器都支持这两种事件传递方式,但是在IE8及以下版本的浏览器中,只支持事件冒泡,不支持事件捕获。

所以DOM中的事件处理分为以下三个阶段

  • capture(捕获阶段),事件由外层向内层传递
  • target(命中阶段),事件到达目标元素
  • bubbling(冒泡阶段),事件由内层向外层传递

那么如何指定事件的传递方式呢?我们可以通过addEventListener的第三个参数来指定,比如下面的代码:
当useCapture为true时,事件传递方式为事件捕获,当useCapture为false时,事件传递方式为事件冒泡。默认值为false,使用事件冒泡模式。

1
addEventListener(type, listener, useCapture)

Event.stopPropagation

  1. 当事件传递方式为捕获模式时,event.stopPropagation()会阻止事件继续向下(内层元素)传递。
  2. 当事件传递方式为冒泡模式时,event.stopPropagation()会阻止事件继续向上(外层元素)传递。

代码示例:

1
2
3
4
5
6
7
8
9
<div id="div1">
div1
<div id="div2">
div2
<div id="div3">
div3
</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
const div1 = document.querySelector("#div1");
bindEventListener(div1, "click", clickHandler1, false);
const div2 = document.querySelector("#div2");
bindEventListener(div2, "click", clickHandler2, false);
const div3 = document.querySelector("#div3");
bindEventListener(div3, "click", clickHandler3, false);

function bindEventListener(element, event, listener, useCapture) {
element.addEventListener(event, listener, useCapture);
}

function clickHandler1(event) {
const text = event.currentTarget.id + " clicked";
console.log(text);
}

function clickHandler2(event) {
const text = event.currentTarget.id + " clicked";
console.log(text);
}

function clickHandler3(event) {
const text = event.currentTarget.id + " clicked";
console.log(text);
}

点击div3,输出如下,因为采用的是冒泡模式,所以事件会从内层元素向外层元素传递。即div3最先捕获事件,然后是冒泡给div2,最后是div1.

1
2
3
div3 clicked
div2 clicked
div1 clicked

如果我们在clickHandler3中加入event.stopPropagation(),再次点击div3,输出如下:

1
div3 clicked

可见,event.stopPropagation()阻止了事件继续向上(外层元素)传递。

将事件处理函数改为捕获模式

1
2
3
bindEventListener(div1, "click", clickHandler1, true);
bindEventListener(div2, "click", clickHandler2, true);
bindEventListener(div3, "click", clickHandler3, true);

再次点击div3,输出如下,因为采用的是捕获模式,所以事件会从外层元素向内层元素传递。即div1最先捕获事件,然后是div2,最后是div3.

1
2
3
div1 clicked
div2 clicked
div3 clicked

如果我们在clickHandler1中加入event.stopPropagation(),再次点击div3,输出如下:

1
div1 clicked

可见,event.stopPropagation()阻止了事件继续向下(内层元素)传递。

Event.stopImmediatePropagation

如果将上述代码中的event.stopPropagation()改为event.stopImmediatePropagation(),你会发现,输出的结果是一样的,这说明event.stopImmediatePropagation()event.stopPropagation()的作用是一样的,都是阻止事件继续传递。既然作用是一样的,那么为什么还要有event.stopImmediatePropagation()呢?这是因为event.stopImmediatePropagation()还有一个额外的功能,就是阻止事件处理函数队列中的其他函数执行,比如下面的代码:

1
2
3
bindEventListener(div1, "click", clickHandler1, false);
bindEventListener(div1, "click", clickHandler2, false);
bindEventListener(div1, "click", clickHandler3, false);

当我们点击div1时,输出如下:

1
2
3
div1 clicked
div1 clicked
div1 clicked

当多个事件处理函数绑定到同一个元素的同一个事件时,事件处理函数的执行顺序是按照绑定的顺序执行的,比如上面的代码,clickHandler1会先于clickHandler2执行,clickHandler2会先于clickHandler3执行。如果我们在clickHandler1中加入event.stopImmediatePropagation(),再次点击div1,输出如下:

1
div1 clicked

可见,event.stopImmediatePropagation()阻止了事件处理函数队列中的其他函数执行。clickHandler2和clickHandler3都被阻止了执行。

阻止默认行为

event.stopPropagation()虽然能阻止事件传播,却不能阻止事件的默认行为,比如将上例中的button换成<a>的话,即使阻止了事件传播,点击链接后a标签依然会跳转。这时,我们可以使用 event.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
<html lang="zh-Hans-CN">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>event-handler</title>
<style>
div#my-div {
text-align: center;
margin: 32px auto;
padding-top: 32px;
width: 400px;
height: 300px;
border: 1px solid gray;
}

a {
display: block;
margin: 16px auto;
}

</style>
</head>
<body>
<div id="my-div">div
<a id='my-button' href="https://www.baidu.com">link</a>
</div>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
document.getElementById('my-div').addEventListener('click', divClickHandler, true);
document.getElementById('my-button').addEventListener('click', buttonClickHandler, true);
function divClickHandler(event) {
event.stopPropagation();
event.preventDefault(); // 阻止链接打开的默认行为
console.log('div clicked');
}
function buttonClickHandler(event) {
console.log('button clicked');
}

有以下几点需要注意:

  1. event.preventDefault()只会阻止事件默认行为,并不会阻止事件继续传播
  2. event.preventDefault()只对cancelable=true的事件起作用。

event.preventDefault()的应用场景有:

  1. 阻止<a>标签点击后跳转

  2. 阻止<checkbox>被选中

  3. 验证用户输入,比如只允许输入小写字母,当输入非小写字母时,不显示输入的字符。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function checkName(evt) {
    var charCode = evt.charCode;
    if (charCode != 0) {
    if (charCode < 97 || charCode > 122) {
    evt.preventDefault();
    displayWarning(
    "Please use lowercase letters only."
    + "\n" + "charCode: " + charCode + "\n"
    );
    }
    }
    }

References