JS中的事件顺序(事件捕获与冒泡)

问题

如果一个元素和它的祖先元素注册了同一类型的事件函数(例如点击等), 那么当事件发生时事件函数调用的顺序是什么呢?

比如, 考虑如下嵌套的元素:

-----------------------------------| outer || ------------------------- || |inner | || ------------------------- || |-----------------------------------

两个元素都有onclick的处理函数. 如果用户点击了inner, innerouter上的事件处理函数都会被调用. 但谁先谁后呢?

两个模型

在刚刚过去的那些糟糕年代, Netscape和M$对此有不同的看法.

Netscape认为outer上的处理函数应该先被执行. 这被称作event capturing.

M$则认为inner上的处理函数具有执行优先权. 这被叫做event bubbling.

两种看法针锋相对

事件捕获(event capturing)

当使用事件捕获时

 | |---------------| |-----------------| outer | | || -----------| |----------- || |inner \ / | || ------------------------- || Event CAPTURING |-----------------------------------

outer上的事件处理器先触发, 然后是inner上的

事件冒泡(event bubbling)

 / ---------------| |-----------------| outer | | || -----------| |----------- || |inner | | | || ------------------------- || Event BUBBLING |-----------------------------------

与事件捕获相反, 当使用事件冒泡时, inner上的事件处理器先被触发, 其后是outer上面的

W3C 模型

W3C标准则取其折中方案. W3C事件模型中发生的任何事件, 先(从其祖先元素window)开始一路向下捕获, 直到达到目标元素, 其后再次从目标元素开始冒泡.

 1. 先从上往下捕获 | | | / -----------------| |--| |-----------------| outer | | | | || -------------| |--| |----------- || | inner \ / | | | || | | | || | 2. 到达目标元素后从下往上冒泡| || -------------------------------- || W3C event model |------------------------------------------

而你作为开发者, 可以决定事件处理器是注册在捕获或者是冒泡阶段. 如果addEventListener的最后一个参数是true, 那么处理函数将在捕获阶段被触发; 否则(false), 会在冒泡阶段被触发.

例如如下的代码:

 var selector = document.querySelector.bind(document); selector(‘div.outer‘).addEventListener(‘click‘, (e) => { selector(‘p:first-of-type‘).textContent += ‘outer clicked! ‘ }, true) selector(‘div.inner‘).addEventListener(‘click‘, (e) => { selector(‘p:first-of-type‘).textContent += ‘inner clicked! ‘ }, false) document.addEventListener(‘click‘, (e) => { selector(‘p:first-of-type‘).textContent += ‘document clicked! ‘ }, true)

当点击inner元素时, 如下事情发生了:

  1. 点击事件开始于捕获阶段. 在此阶段, 浏览器会在inner的所有祖先元素上查找点击事件处理函数(从window开始).

  2. 结果找到了2个, 分别在documentouter上面, 而且这两个事件处理函数的useCapture选项为true, 说明它们是被注册在捕获阶段的. 于是, documentouter的点击处理函数被执行了.

  3. 继续向下寻找, 直到达到inner元素本身. 捕获阶段就此结束. 此时进入冒泡阶段, inner上的事件处理器得到执行.

  4. 事件命中目标元素后开始向上冒泡, 一路查找是否有注册了冒泡阶段的祖先元素上的事件处理器. 由于没有找到, 因此什么也没发生.

最后的结果是:

如果我们把祖先元素的事件处理器注册在冒泡阶段的话(addEventListeneruseCapture选项为false):

 var selector = document.querySelector.bind(document); selector(‘div.outer‘).addEventListener(‘click‘, (e) => { selector(‘p:first-of-type‘).textContent += ‘outer clicked! ‘ console.log(e); }, false) selector(‘div.inner‘).addEventListener(‘click‘, (e) => { selector(‘p:first-of-type‘).textContent += ‘inner clicked! ‘ console.log(e); }, false) document.addEventListener(‘click‘, (e) => { selector(‘p:first-of-type‘).textContent += ‘document clicked! ‘ }, false)

结果则是:

传统模型

element.onclick = function(){}

将被注册在冒泡阶段.

事件冒泡的应用

例如: 当点击时的默认函数

如果在document上注册一个点击函数:

document.addEventlistener(‘click‘, (e) => {}, false)

那么任何元素上的点击事件最后都会冒泡到这个事件处理器上并触发函数 - 除非前面的事件处理函数阻止了冒泡(e.stopPropogation(), 在这种情况下事件不会继续向上冒泡)

注意: e.stopPropagation()只能阻止事件在冒泡阶段的向上传播. 如果被点击元素的祖先元素有注册在捕获阶段的事件处理器:

ancestorElem.addEventListner(‘click‘, (e) => { // do something... }, true)

那么该祖先元素上的事件处理器照样会在捕获阶段被触发.

因此, 你可以在document上设置这么一个处理函数, 当页面上的任何元素被点击时, 这个处理函数就被会触发. 一个实用的例子就是下拉菜单: 当点击文档上除下拉菜单本身时任意一处时, 下拉菜单会被隐藏.

在冒泡或者捕获阶段, e.currentTarget指向当前事件处理函数所附着的元素. 你也可以用事件处理函数内的this取而代之.

M$模型的麻烦

在M$模型中, 没有对e.currentTarget的支持, 更糟糕的是, this也不指向当前的HTML元素.

相关文章