两个模型虽然从思路上南辕北辙,但是都可以保证所有绑定的回调函数正确触发(不过触发顺序是相反的。如果这个触发顺序很重要,那么在当时,你的代码可能只能在一个浏览器中正确运行,或者去做恶心的浏览器兼容)。不过浏览器允许开发者在事件传播的过程中阻止事件的继续传播,此时两者的差异就变得极其明显。
假如我们在定义点击div元素的回调函数时阻止了事件的传播:
div.addEventListener("click", function(e){ ... e.stopPropagation(); //阻止事件继续传播 })
这个代码会在两种模型下产生巨大的差异。在捕获模型中,由于最外部首先得到该事件,因此body的点击事件首先被触发,之后是div的点击事件。由于阻止了事件传播,p元素不会触发回调。而在冒泡模型中则恰恰相反,内部的p首先得到该事件,其次才是div,因此触发回调的将是p和div,body因为事件没有冒泡上来而无法监听到该事件。同样的代码在两种模型中产生了完全不同的行为,这对于开发者来说显然是不可接受的(两个模型都有自己的适用场景,也都有自己的合理性,因此对于模型的好坏不能一概而论)。
那么后来的国际标准组织是如何解决这个冲突的呢?答案就是由开发者自己选择。
标准的事件绑定使用addEventListener函数,它接收两个必传参数和一个可选参数:必传的为event(事件名,如"cick")和function(回调函数),可选的为useCapture(是否使用捕获模型,默认为false,根据MDN的接口说明,这里也可以传入一个对象,为本次监听设置其他参数,详细请参考MDN接口文档 - addEventListener)。
div.addEventListener("click", function(){}, true); //使用捕获模型
第三个参数就是标识开发者是否需要使用捕获模型,默认为false,也就是默认使用微软的冒泡模型(这是因为大多数事件都只在最内部的元素上触发,这也间接表明,冒泡模型的普适性更好)。如果开发者的需求确实需要使用捕获模型,可以将第三个参数设置为true。比如下面的例子:
事件捕获与冒泡的用法
了解了事件捕获与冒泡的基本原理之后,我们举个例子来说明这两个模型的基本用法。
假设有以下的DOM结构:
<div> <div> </div> </div>
这是两个重叠的div,当点击时,两者都会响应这个click事件。假如事件绑定如下:
var outer = document.querySelector("#outer"); var inner = document.querySelector("#inner"); outer.addEventListener("click", function(e){ alert("来自外部div的消息"); e.stopPropagation(); //阻止事件向内部传播 }, true); //使用捕获模型 inner.addEventListener("click", function(e){ alert("来自内部div的消息"); }, true); //使用捕获模型
页面上将只显示外部弹出的消息,内部的事件被e.stopPropagation()拦截了下来,导致事件没有触发。而如果写成下面的代码:
var outer = document.querySelector("#outer"); var inner = document.querySelector("#inner"); outer.addEventListener("click", function(e){ alert("来自外部div的消息"); }, false); //使用冒泡模型 inner.addEventListener("click", function(e){ alert("来自内部div的消息"); e.stopPropagation(); //阻止事件向外部传播 }, false); //使用冒泡模型
这次是只显示了内部的消息,而没有显示外部的消息,说明事件在向上冒泡的过程中被阻止了。
注意
如果是在表格中内嵌复选框,希望实现点击一行时选中复选框,通过stopPropagation阻止CheckBox响应click事件并不能实现。测试发现复选框状态改变的事件似乎并不是在click事件触发的(断点跟踪表明,CheckBox在执行click回调之前,状态就已经发生了改变,具体是通过什么事件改变了选中状态尚不清楚),下面给一个可以处理行点击的示例:
<table cellspacing="0"> <tr> <td> <input type="checkbox"> </td> <td> 表格第一行 </td> </tr> <tr> <td> <input type="checkbox"> </td> <td> 表格第二行 </td> </tr> </table> <script> var tr = document.querySelectorAll(".tr"); //获取所有tr tr.forEach(function(item){ //为每个tr绑定click事件,手动选中复选框 item.addEventListener("click", function(e){ var checkbox = item.querySelector(".checkbox"); checkbox.checked = !checkbox.checked; }) }) var cb = document.querySelectorAll(".checkbox"); cb.forEach(function(item){ item.addEventListener("click", function(e){ this.checked = !this.checked; }); }) </script>