DOM 事件流

这篇笔记主要简述下 DOM 事件流 - DOM Event Flow. 用于理解事件捕获与冒泡.

什么是事件

事件, 就是文档或浏览器窗口发生的一些特定的交互瞬间. JavaScript 和 HTML 之间的交互是通过事件实现的. 可以使用事件监听器来监听事件, 以便事件发生时执行相应的代码.

事件流

当在页面上某个元素触发特定事件时, 比如点击, 除了被点击的目标元素, 所有祖先元素都会触发该事件, 一直到 window.

那这样就出现了一个问题, 是先在目标元素上触发事件, 还是先在祖先元素上触发呢? 这就是事件流的概念.

事件流是事件在目标元素和祖先元素间的触发顺序, 在早期,微软和网景实现了相反的事件流, 网景主张捕获方式, 微软主张冒泡方式:

  • 捕获 - Capture - 事件由最顶层逐级向下传播, 直至到达目标元素.
  • 冒泡 - Bubble - 顾名思义, 类似水中冒泡, 从下往上. 事件由第一个被触发的元素接收, 然后逐级向上传播.

后来 w3c 采用折中的方式, 规定先捕获再冒泡平息了战火. 如此一个事件就被分成了三个阶段(是的, 不光是捕获和冒泡):

  1. 捕获阶段 - The capture phase - 事件从最顶层元素 window 一直传递到目标元素的父元素.
  2. 目标阶段 - The target phase - 事件到达目标元素. 如果事件指定不冒泡. 那就会在这里中止.
  3. 冒泡阶段 - The bubble phase - 事件从目标元素父元素向上逐级传递直到最顶层元素 window. 及捕获阶段的反方向.

那这里又有一个新的疑问, 既然捕获和冒泡阶段都会触发事件, 那先捕获再冒泡, 岂不是路径上的元素都会触发两次事件?

在 DOM2 中, 事件监听机制提供了一个参数来决定事件是在捕获阶段生效还是在冒泡阶段生效, 接下来简要学习下 addEventListener.

还有一个小问题, 为什么要单独区分一个目标阶段? 笔记接下来的注意选项框有解答.

addEventListener

EventTarget.addEventListener() 方法将指定的监听器注册到目标元素上, 当该对象触发指定的事件时, 指定的回调函数就会被执行. 事件目标可以是一个文档上的元素或任何其他支持事件的对象 (比如 XMLHttpRequest).

它的工作原理是将实现EventListener的函数或对象添加到调用它的 EventTarget 上的指定事件类型的事件侦听器列表中.

语法

target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);
target.addEventListener(type, listener[, useCapture, wantsUntrusted  ]);  // Gecko/Mozilla only
  • type: 表示监听事件类型的字符串. 事件列表.
  • listener: 当所监听的事件类型触发时的回调. 会接收到一个事件通知对象.
  • options: 可选. 可用的选项如下:
    • capture: Boolean, 如果是 true, 表示 listener 会在捕获阶段触发. 默认是 false. 冒泡捕获. 所以微软牛逼.
    • once: Boolean, 如果是 true, 表示 listener 在添加之后最多只调用一次.
    • passive: Boolean,如果是, 表示 listener 永远不会调用 preventDefault(). 如果 listener 仍然调用了这个函数, 客户端将会忽略它并抛出一个控制台警告.
  • useCapture: 可选. Boolean, 同 options - capture

具体使用参考另一篇笔记. TODO

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

事件监听添加与移除

添加事件监听有三种方式:

  1. 通过 HTML 属性的方式
<div onclick="console.log('click')"></div>
  1. DOM0 中可以通过js脚本来给指定元素提供事件处理函数,即
element.onclick = function // 只会在冒泡阶段生效
  1. DOM2 中, 添加了新的事件监听API, 即addEventListener, 同时提供了取消监听的 removeEventListener(type, handler[, options | useCapture]). 显然事件处理函数注册后, 要取消监听, type/hanlder/useCapture的一致.

相比第一, 第二种, addEventListener的方式有以下几点优势:

  • 可以为同一个事件注册多个回调函数, 依次触发. 而 DOM0 的方式则会被覆盖掉. 只能添加一种
  • 使用 DOM0 会覆盖 HTML 的方式.
  • 可以通过参数决定监听是在冒泡阶段生效还是在捕获阶段生效.
  • element.onclick 注册的监听只会在冒泡阶段生效

例子:

<div id="parent">
    <div id="child" onclick="console.log('html')"/>
</div>

const child = document.getElementById('child') 

child.onclick = function () { console.log('DOM0A') } // 覆盖了 HTML 的方式
child.onclick = function () { console.log('DOM0B') } // 覆盖了上一条 DOM0A   

child.addEventLisnter('click', function () { console.log('lisnterA') })
child.addEventLisnter('click', function () { console.log('lisnterB') }) // 不会覆盖

三种方式对应的如何移除:

child.setAttributer('onclick', false)
child.onclick = null
child.removeEventLisnter('设置相同的参数')

三种方式优先级排序 onclick > html > addEventLisnter. onclick 会覆盖 html. 但是 addEventLisnter 不会被覆盖且可以叠加.

问题

  1. 以下代码的输出顺序
<html onclick="console.log('html')">

<head>
    <meta charset="UTF-8">

    <title>Test</title>
    <script type="text/javascript">
        window.onload = function() {
            const parent = document.getElementById("parent");
            const child = document.getElementById("child");

            child.onclick = function () { console.log('child onclick') };

            // capture 默认为 false, 冒泡
            parent.addEventListener('click', function () { console.log('parent event') });
            child.addEventListener('click', function () { console.log('child event') });

            // 捕获
            parent.addEventListener('click', function () { console.log('parent event capture') }, { capture: true });
            child.addEventListener('click', function () { console.log('child event capture') }, { capture: true });
        }
        window.onclick = function() {
            console.log('window');
        }
    </script>
</head>

<body onclick="console.log('body')">
    <div id="parent" onclick="console.log('parent html')">parent
        <div id="child" onclick="console.log('child html')">child</div>
    </div>
</div>
</body>

</html>
  1. 什么是事件流,同一个事件有几个阶段?
  2. 如何移除通过HTML属性、element.onclick等注册的事件处理函数?

问题1答案

首先参考 chrome 浏览器的结果. 先来看点击 child, 输出:

  1. parent html
  2. parent event
  3. parent event capture
  4. body
  5. html
  6. window

奇怪! 这时候不是应该先捕获, capture 在前吗? 别急, 这时候如果调换 parent event 与 parent event capture 的 addEventListener 顺序, 会发现, 输出顺序也变了:

  1. parent html
  2. parent event capture
  3. parent event
  4. body
  5. html
  6. window

其实这时候的输出顺序只和 addEventListener 的顺序有关. 是否开启 capture 无关. 因为我们是直接点击了 parent. 对于 parent 自身而言. 它在整个点击传递过程中处于目标阶段. 并不涉及捕获和冒泡. 同时我们也发现. html 设置的点击是优先于 addEventListener 的方式的.

我们再来看下点击 child:

  1. parent event capture
  2. child onclick
  3. child event
  4. child event capture
  5. parent html
  6. parent event
  7. body
  8. html
  9. window

首先, child html 没有输出, 因为被 child onclick 覆盖掉了.
第二点, child onclick 的优先级也是高于 addEventListener 的.
再然后, 点击事件传递过程中, 首先在捕获阶段, 输出了 parent event capture. 然后到达目标阶段. 同理之前点击 parent. 如果此时调换 child event 与 child event capture 的 addEventListener 顺序. 输出顺序也会改变.
最后, 到达冒泡阶段, 由于 html 的优先级高于 addEventListener. 所以被输出. (别忘了, 默认都是开启冒泡)

但是. 如果使用 Safari 浏览器, 则会发现, 点击 parent, 输出:

  1. parent event capture
  2. parent html
  3. parent event
  4. body
  5. html
  6. window

点击 child, 输出:

  1. parent event capture
  2. child event capture
  3. child onclick
  4. child event
  5. parent html
  6. parent event
  7. body
  8. html
  9. window

会发现, 对于捕获阶段的处理不一样了. 点击目标元素时, 目标元素不仅处于目标阶段, 也处于捕获阶段的终点和冒泡阶段的起点. 所以会进行 capture 的判断. 所以在代码编写时这里要尤为注意.

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容