(共566篇)
全部分类

事件流
[ JS基础 ] 

事件流

先来观察一个案例:

 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
<style>
    body {
        margin: 0;
        background-color: darksalmon;
    }
    div {
        margin: 0 auto;
    }
    .outer {
        width: 200px;
        height: 200px;
        background-color: aqua;
    }
    .inner {
        width: 100px;
        height: 100px;
        background-color: cadetblue;
    }
</style>

<div class="outer">
    <div class="inner"></div>
</div>

<script>
    var outerEle = document.querySelector(".outer");
    var innerEle = document.querySelector(".inner");

    document.body.addEventListener("click", function() {
        console.log("点击了body");
    });
    outerEle.addEventListener("click", function() {
        console.log("点击了.outer元素");
    });
    innerEle.addEventListener("click", function() {
        console.log("点击了.inner元素");
    });
</script>

案例源码

在这个案例中,我给body .outer .inner都添加了点击事件,.inner.outer的后代元素,.outer又是body的后代元素,点击了.inner就相当于点击了.outerbody。如果我在.inner上点击一次,它们三个的事件谁的侦听函数优先执行呢?

最初的时候,Netscape 公司决定这种事件依次从外向内执行,即先执行body上的侦听函数,再执行.outer上的侦听函数,最后执行.inner上的侦听函数。

而 IE 公司就调皮了,为了体现出自己的地位,偏要不走寻常路。就是要反过来从内往外执行,即先执行.inner的侦听函数,再执行.outer的侦听函数,最后执行body的侦听函数。

这两种截然不同的执行方式,都叫做事件流

从外而内的执行方式,被称为事件捕获,从内而外的执行方式,被称为事件冒泡, 。两个公司的政策不同,被坑的只有开发人员。后来为了 W3C 组织为了统一规范,决定把两者合二为一:

如果父子元素都添加了相同的监听事件,先执行事件捕获,再执行事件冒泡。也就是说: 先从外向内执行侦听函数,再从内向外执行函数。

这样一来,元素的事件流就分为了三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段

并且新增了element.addEventListener(eventType, fn [, useCapture])函数,可以由该函数的第三个参数,决定添加的侦听函数具体在哪个阶段执行。useCapture的默认值是false,表示改侦听函数会在事件冒泡阶段执行。

所以上面代码的执行结果应该是这样的:

如果把useCapture设置为 true,就表示此处添加的侦听函数会在事件捕获阶段执行:

 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
44
45
46
47
48
49
50
<style>
    body {
        margin: 0;
        background-color: darksalmon;
    }
    div {
        margin: 0 auto;
    }
    .outer {
        width: 200px;
        height: 200px;
        background-color: aqua;
    }
    .inner {
        width: 100px;
        height: 100px;
        background-color: cadetblue;
    }
</style>

<div class="outer">
    <div class="inner"></div>
</div>

<script>
    var outerEle = document.querySelector(".outer");
    var innerEle = document.querySelector(".inner");

    document.body.addEventListener(
        "click",
        function() {
            console.log("点击了body");
        },
        true,
    );
    outerEle.addEventListener(
        "click",
        function() {
            console.log("点击了.outer元素");
        },
        true,
    );
    innerEle.addEventListener(
        "click",
        function() {
            console.log("点击了.inner元素");
        },
        true,
    );
</script>

案例源码

这里三个侦听函数都被设置为在事件捕获阶段执行,所以body的侦听函数优先执行,接着执行.outer的侦听函数,最后执行.inner的侦听函数。

现在为了验证统一后的事件流,我们给body .outer .inner分别添加 1 个事件捕获阶段的侦听函数,和 1 个事件冒泡阶段的侦听函数:

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<style>
    body {
        margin: 0;
        background-color: darksalmon;
    }
    div {
        margin: 0 auto;
    }
    .outer {
        width: 200px;
        height: 200px;
        background-color: aqua;
    }
    .inner {
        width: 100px;
        height: 100px;
        background-color: cadetblue;
    }
</style>

<div class="outer">
    <div class="inner"></div>
</div>

<script>
    var outerEle = document.querySelector(".outer");
    var innerEle = document.querySelector(".inner");

    document.body.addEventListener(
        "click",
        function() {
            console.log("捕获阶段触发body");
        },
        true,
    );
    document.body.addEventListener("click", function() {
        console.log("冒泡阶段触发body");
    });
    outerEle.addEventListener(
        "click",
        function() {
            console.log("捕获阶段触发outer");
        },
        true,
    );
    outerEle.addEventListener("click", function() {
        console.log("冒泡阶段触发outer");
    });
    innerEle.addEventListener(
        "click",
        function() {
            console.log("捕获阶段触发inner");
        },
        true,
    );
    innerEle.addEventListener("click", function() {
        console.log("冒泡阶段触发inner");
    });
</script>

案例源码

从控制台中可以看出,添加的这些侦听函数,确实是先执行了定义在捕获阶段执行的函数,再执行定义在冒泡阶段的函数。

注意

现在我们把前面的代码顺序调整一下:

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<style>
    body {
        margin: 0;
        background-color: darksalmon;
    }
    div {
        margin: 0 auto;
    }
    .outer {
        width: 200px;
        height: 200px;
        background-color: aqua;
    }
    .inner {
        width: 100px;
        height: 100px;
        background-color: cadetblue;
    }
</style>

<div class="outer">
    <div class="inner"></div>
</div>

<script>
    var outerEle = document.querySelector(".outer");
    var innerEle = document.querySelector(".inner");

    document.body.addEventListener(
        "click",
        function() {
            console.log("捕获阶段触发body");
        },
        true,
    );
    document.body.addEventListener("click", function() {
        console.log("冒泡阶段触发body");
    });
    outerEle.addEventListener(
        "click",
        function() {
            console.log("捕获阶段触发outer");
        },
        true,
    );
    outerEle.addEventListener("click", function() {
        console.log("冒泡阶段触发outer");
    });
    innerEle.addEventListener("click", function() {
        console.log("冒泡阶段触发inner");
    });
    innerEle.addEventListener(
        "click",
        function() {
            console.log("捕获阶段触发inner");
        },
        true,
    );
</script>

案例源码

这个案例和上一个案例唯一不同之处在于,关于inner元素桑的两种侦听函数的添加顺序不同。导致最终结果不同,这是因为:

  1. 事件流包含三个阶段:捕获阶段-处于目标阶段-冒泡阶段
  2. 祖先元素的侦听函数,会根据useCapture参数的值,在捕获阶段或者冒泡阶段执行
  3. 触发事件的目标元素上的侦听函数,则不受useCapture参数的影响,会按照侦听函数添加的先后顺序执行。

addEventListener 的第二种用法

element.addEventListener()函数还有第二种用法:element.addEventListener(type, fn [, options]),option可以传入一个对象,在这个对象中也可以设置侦听函数的执行方式。

option 属性 作用
capture 设置为true,表示侦听函数会在事件捕获阶段执行,默认是false
once 设置为true,表示侦听函数只执行一次,默认是 false
 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
44
45
46
47
48
49
50
51
52
53
54
55
56
<style>
    body {
        margin: 0;
        background-color: darksalmon;
    }
    div {
        margin: 0 auto;
    }
    .outer {
        width: 200px;
        height: 200px;
        background-color: aqua;
    }
    .inner {
        width: 100px;
        height: 100px;
        background-color: cadetblue;
    }
</style>

<div class="outer">
    <div class="inner"></div>
</div>

<script>
    var outerEle = document.querySelector(".outer");
    var innerEle = document.querySelector(".inner");

    document.body.addEventListener(
        "click",
        function() {
            console.log("点击了body");
        },
        {
            capture: true,
        },
    );
    outerEle.addEventListener(
        "click",
        function() {
            console.log("点击了.outer元素");
        },
        {
            capture: true,
        },
    );
    innerEle.addEventListener(
        "click",
        function() {
            console.log("点击了.inner元素");
        },
        {
            capture: true,
        },
    );
</script>

案例源码

为了兼容早期版本的 IE 浏览器,一般很少会把侦听事件放在捕获阶段执行