JavaScript 事件绑定、冒泡、捕获与执行顺序详解

作者:窗边听雨

时间:2026-10-30

浏览:145

本文系统梳理了 JavaScript 中 DOM 事件的多种绑定方式,对比了 HTML 内联、JS 属性及 W3C 标准 API 的优劣。深入解析了事件冒泡与捕获的核心概念,结合 DOM 事件流的三个阶段(捕获、目标、冒泡),详细说明了不同绑定参数对执行顺序的影响,并提供了阻止事件传播的标准做法。

文章目录

近期深入研究了 JavaScript 与 jQuery 的事件机制,收获良多,特此整理成文与大家分享。

事件绑定的几种方式

(一) 事件绑定的几种方式

在 JavaScript 中为 DOM 元素绑定事件处理函数,主要存在两种途径:一是在 HTML 文档中直接绑定,二是在 JavaScript 代码中绑定。其中,方式 1 和方式 2 属于 HTML 内联绑定,方式 3、方式 4 和方式 5 属于 JavaScript 代码绑定。在这几种方法中,方式 5 被公认为最佳实践。

方式 1:

HTML 的 DOM 元素支持以 on 开头的属性,如 onclick、onblur 等,我们可以直接在属性值中编写 JavaScript 代码。例如,当点击 div 时,以下代码会弹出该 div 的 ID:

<div id="outestA" onclick="var id = this.id;alert(id);return false;"></div>

这种做法显然不可取,因为代码被包裹在字符串中,无法进行格式化和排版,代码量大时极难阅读。需要说明的是,onclick 属性中的 this 指向当前被点击的 DOM 对象,因此我们可以通过 this.id 获取 DOM 元素的 id 属性值。

方式 2:

当代码量较大时,我们可以在 onclick 等属性中指定函数名。


<script>

 function buttonHandler(thisDom)
 {
 alert(this.id);//undefined
 alert(thisDom.id);//outestA
 return false;
 }
</script>
<div id="outestA" onclick="return buttonHandler(this);"></div>

与直接内联代码相比,这种做法略有改善。值得注意的是:在 onclick 属性值中指定的事件处理函数里,this 指向的是 window 对象,因此我们需要通过 this 将 DOM 对象作为参数传递进去。

方式 3:在 JS 代码中通过 dom 元素的 onclick 等属性


var dom = document.getElementById("outestA");
dom.onclick = function(){alert("1=" + this.id);};
dom.onclick = function(){alert("2=" + this.id);};

在这种做法中,this 指向当前的 DOM 对象。此外,这种方式只能绑定一个事件处理函数,后续绑定的函数会覆盖前面的函数。

方式 4:IE 下使用 attachEvent/detachEvent 函数进行事件绑定和取消。

attachEvent/detachEvent 的兼容性较差,虽然 IE6~IE11 支持该函数,但 FF 和 Chrome 浏览器不支持该方法。而且 attachEvent/detachEvent 并非 W3C 标准做法,因此不推荐使用。在 IE 浏览器下,attachEvent 具有以下特点。

a) 事件处理函数中的 this 指向的是 window 对象,而非 DOM 对象。


var dom = document.getElementById("outestA"); 
dom.attachEvent('onclick',a); 
   
function a() 
{  
  alert(this.id);//undefined 
}

b) 同一个事件处理函数只能绑定一次。


var dom = document.getElementById("outestA"); 
dom.attachEvent('onclick',a); 
dom.attachEvent('onclick',a);  
function a() 
{ 
  alert(this.id);
}

即使使用 attachEvent 绑定了 2 次,函数 a 也只会调用一次。

c) 不同的函数对象可以重复绑定,不会相互覆盖。


var dom = document.getElementById("outestA"); 
dom.attachEvent('onclick',function(){alert(1);}); 
dom.attachEvent('onclick',function(){alert(1);}); // 当outestA的click事件发生时,会弹出2个对话框 

匿名函数之间互不相同,即使代码完全一样。因此,如果我们想用 detachEvent 取消 attachEvent 绑定的事件处理函数,绑定事件时不能使用匿名函数,必须将事件处理函数单独定义为一个具名函数,否则无法取消。

方式 5:使用 W3C 标准的 addEventListener 和 removeEventListener。

这 2 个函数是 W3C 标准规定的,FF 和 Chrome 浏览器都支持,IE6/IE7/IE8 都不支持这 2 个函数。不过从 IE9 开始就支持了这 2 个标准的 API。


// type:事件类型,不含"on",比如"click"、"mouseover"、"keydown";
// 而attachEvent的事件名称,含含"on",比如"onclick"、"onmouseover"、"onkeydown";
// listener:事件处理函数
// useCapture是事件冒泡,还是事件捕获,默认false,代表事件冒泡类型
addEventListener(type, listener, useCapture); 

a) 事件处理函数中的 this 指向的是 DOM 对象,而非 window,这个特性与 attachEvent 不同。


var dom = document.getElementById("outestA"); 
dom.addEventListener('click', a, false); 
   
function a() 
{  
  alert(this.id);//outestA 
}

b) 同一个事件处理函数可以绑定 2 次,一次用于事件捕获,一次用于事件冒泡。


var dom = document.getElementById("outestA"); 
dom.addEventListener('click', a, false); 
dom.addEventListener('click', a, true); 
   
function a() 
{  
  alert(this.id);//outestA 
}// 当点击outestA的时候,函数a会调用2次

如果绑定的是同一个事件处理函数,并且都是事件冒泡类型或者事件捕获类型,那么只能绑定一次。


var dom = document.getElementById("outestA"); 
dom.addEventListener('click', a, false); 
dom.addEventListener('click', a, false); 
   
function a() 
{  
  alert(this.id);//outestA 
}

// 当点击outestA的时候,函数a只会调用1次

c) 不同的事件处理函数可以重复绑定,这个特性与 attachEvent 一致。

事件处理函数的执行顺序

(二) 事件处理函数的执行顺序

方式 1、方式 2 和方式 3 都不能实现事件的重复绑定,所以自然也就不存在执行顺序的问题。方式 4 和方式 5 可以重复绑定特性,所以需要了解下执行顺序的问题。如果你写出依赖于执行顺序的代码,可以断定你的设计存在问题。所以下面的顺序问题,仅作为兴趣探讨,没有什么实际意义。直接上结论:addEventListener 和 attachEvent 表现一致,如果给同一个事件绑定多个处理函数,先绑定的先执行。下面的代码我在 IE11、FF17 和 Chrome39 都测试过。


<script>
 window.onload = function(){
 <span style="white-space:pre"> </span>var outA = document.getElementById("outA"); 
 outA.addEventListener('click',function(){alert(1);},false);
 outA.addEventListener('click',function(){alert(2);},true);
 outA.addEventListener('click',function(){alert(3);},true);
 outA.addEventListener('click',function(){alert(4);},true);
 };
</script>

<body>
 <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">
 </div>
</body>

当点击 outA 的时候,会依次打印出 1、2、3、4。这里特别需要注意:我们给 outA 绑定了多个 onclick 事件处理函数,也是直接点击 outA 触发的事件,所以不涉及事件冒泡和事件捕获的问题,即 addEventListener 的第三个参数在这种场景下,没有什么用处。如果是通过事件冒泡或者是事件捕获触发 outA 的 click 事件,那么函数的执行顺序会有变化。

事件冒泡和事件捕获

(三) 事件冒泡和事件捕获

事件冒泡和事件捕获很好理解,只不过是对同一件事情的不同看法,只不过这 2 种看法都很有道理。 我们知道 HTML 中的元素是可以嵌套的,形成类似于树的层次关系。比如下面的代码:


<div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">
 <div id="outB" style="height:200; background:#0000ff;top:100px;position:relative;">
 <div id="outC" style="height:100px; background:#FFB90F;top:50px;position:relative;"></div> 
 </div>
</div>

如果点击了最内侧的 outC,那么外侧的 outB 和 outC 算不算被点击了呢?很显然算,不然就没有必要区分事件冒泡和事件捕获了,这一点各个浏览器厂家也没有什么疑义。假如 outA、outB、outC 都注册了 click 类型事件处理函数,当点击 outC 的时候,触发顺序是 A-->B-->C,还是 C-->B-->A 呢?如果浏览器采用的是事件冒泡,那么触发顺序是 C-->B-->A,由内而外,像气泡一样,从水底浮向水面;如果采用的是事件捕获,那么触发顺序是 A-->B-->C,从上到下,像石头一样,从水面落入水底。

事件冒泡见下图:

事件绑定的几种方式

事件捕获见下图:

事件绑定的几种方式

一般来说事件冒泡机制,用的更多一些,所以在 IE8 以及之前,IE 只支持事件冒泡。IE9+/FF/Chrome 这 2 种模型都支持,可以通过 addEventListener((type, listener, useCapture) 的 useCapture 来设定,useCapture=false 代表着事件冒泡,useCapture=true 代表着采用事件捕获。


<script>

 window.onload = function(){
 var outA = document.getElementById("outA"); 
 var outB = document.getElementById("outB"); 
 var outC = document.getElementById("outC"); 
 
 // 使用事件冒泡
 outA.addEventListener('click',function(){alert(1);},false);
 outB.addEventListener('click',function(){alert(2);},false);
 outC.addEventListener('click',function(){alert(3);},false);
 };
 
</script>

<body>
 <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">
 <div id="outB" style="height:200; background:#0000ff;top:100px;position:relative;">
  <div id="outC" style="height:100px; background:#FFB90F;top:50px;position:relative;"></div> 
 </div>
 </div>
</body>

使用的是事件冒泡,当点击 outC 的时候,打印顺序是 3-->2-->1。如果将 false 改成 true 使用事件捕获,打印顺序是 1-->2-->3。

DOM 事件流

(四) DOM 事件流

DOM 事件流我也不知道怎么解释,个人感觉就是事件冒泡和事件捕获的结合体,直接看图吧。

事件绑定的几种方式

DOM 事件流:将事件分为三个阶段:捕获阶段、目标阶段、冒泡阶段。先调用捕获阶段的处理函数,其次调用目标阶段的处理函数,最后调用冒泡阶段的处理函数。这个过程很类似于 Struts2 框中的 action 和 Interceptor。当发出一个 URL 请求的时候,先调用前置拦截器,其次调用 action,最后调用后置拦截器。


<script>

 window.onload = function(){
 var outA = document.getElementById("outA"); 
 var outB = document.getElementById("outB"); 
 var outC = document.getElementById("outC"); 
 
 // 目标(自身触发事件,是冒泡还是捕获无所谓)
 outC.addEventListener('click',function(){alert("target");},true);
 
 // 事件冒泡
 outA.addEventListener('click',function(){alert("bubble1");},false);
 outB.addEventListener('click',function(){alert("bubble2");},false);
 
 // 事件捕获
 outA.addEventListener('click',function(){alert("capture1");},true);
 outB.addEventListener('click',function(){alert("capture2");},true); 
 };
 
</script>

<body>
 <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">
 <div id="outB" style="height:200; background:#0000ff;top:100px;position:relative;">
  <div id="outC" style="height:100px; background:#FFB90F;top:50px;position:relative;"></div> 
 </div>
 </div>
</body>

当点击 outC 的时候,依次打印出 capture1-->capture2-->target-->bubble2-->bubble1。到这里是不是可以理解 addEventListener(type,handler,useCapture) 这个 API 中第三个参数 useCapture 的含义呢?useCapture=false 意味着:将事件处理函数加入到冒泡阶段,在冒泡阶段会被调用;useCapture=true 意味着:将事件处理函数加入到捕获阶段,在捕获阶段会被调用。从 DOM 事件流模型可以看出,捕获阶段的事件处理函数,一定比冒泡阶段的事件处理函数先执行。

事件函数执行先后顺序

(五) 再谈事件函数执行先后顺序

在 DOM 事件流中提到过:

// 目标(自身触发事件,是冒泡还是捕获无所谓) outC.addEventListener('click',function(){alert("target");},true);

我们在 outC 上触发 onclick 事件 (这个是目标对象),如果我们在 outC 上同时绑定捕获阶段/冒泡阶段事件处理函数会怎么样呢?


<script>

 window.onload = function(){
 var outA = document.getElementById("outA"); 
 var outB = document.getElementById("outB"); 
 var outC = document.getElementById("outC"); 
 
 // 目标(自身触发事件,是冒泡还是捕获无所谓)
 outC.addEventListener('click',function(){alert("target2");},true);
 outC.addEventListener('click',function(){alert("target1");},true);
 
 // 事件冒泡
 outA.addEventListener('click',function(){alert("bubble1");},false);
 outB.addEventListener('click',function(){alert("bubble2");},false);
 
 // 事件捕获
 outA.addEventListener('click',function(){alert("capture1");},true);
 outB.addEventListener('click',function(){alert("capture2");},true);

 
 
 };
 
</script>

<body>
 <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">
 <div id="outB" style="height:200; background:#0000ff;top:100px;position:relative;">
  <div id="outC" style="height:100px; background:#FFB90F;top:50px;position:relative;"></div> 
 </div>
 </div>
</body>

点击 outC 的时候,打印顺序是:capture1-->capture2-->target2-->target1-->bubble2-->bubble1。由于 outC 是我们触发事件的目标对象,在 outC 上注册的事件处理函数,属于 DOM 事件流中的目标阶段。目标阶段函数的执行顺序:先注册的先执行,后注册的后执行。这就是上面我们说的,在目标对象上绑定的函数是采用捕获,还是采用冒泡,都没有什么关系,因为冒泡和捕获只是对父元素上的函数执行顺序有影响,对自己没有什么影响。如果不信,可以将下面的代码放进去验证。


// 目标(自身触发事件,是冒泡还是捕获无所谓)
outC.addEventListener('click',function(){alert("target1");},false);
outC.addEventListener('click',function(){alert("target2");},true);
outC.addEventListener('click',function(){alert("target3");},true);
outC.addEventListener('click',function(){alert("target4");},false);

至此我们可以给出事件函数执行顺序的结论了:捕获阶段的处理函数最先执行,其次是目标阶段的处理函数,最后是冒泡阶段的处理函数。目标阶段的处理函数,先注册的先执行,后注册的后执行。

阻止事件冒泡和捕获

(六) 阻止事件冒泡和捕获

默认情况下,多个事件处理函数会按照 DOM 事件流模型中的顺序执行。如果子元素上发生某个事件,不需要执行父元素上注册的事件处理函数,那么我们可以停止捕获和冒泡,避免没有意义的函数调用。前面提到的 5 种事件绑定方式,都可以实现阻止事件的传播。由于第 5 种方式,是最推荐的做法。所以我们基于第 5 种方式,看看如何阻止事件的传播行为。IE8 以及以前可以通过 window.event.cancelBubble=true 阻止事件的继续传播;IE9+/FF/Chrome 通过 event.stopPropagation() 阻止事件的继续传播。


<script>

 window.onload = function(){
 var outA = document.getElementById("outA"); 
 var outB = document.getElementById("outB"); 
 var outC = document.getElementById("outC"); 
 
 // 目标
 outC.addEventListener('click',function(event){
  alert("target");
  event.stopPropagation();
 },false);

 // 事件冒泡
 outA.addEventListener('click',function(){alert("bubble");},false);

 // 事件捕获
 outA.addEventListener('click',function(){alert("capture");},true); 
 
 };
 
</script>

<body>
 <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">
 <div id="outB" style="height:200; background:#0000ff;top:100px;position:relative;">
  <div id="outC" style="height:100px; background:#FFB90F;top:50px;position:relative;"></div> 
 </div>
 </div>
</body>

当点击 outC 的时候,之后打印出 capture-->target,不会打印出 bubble。因为当事件传播到 outC 上的处理函数时,通过 stopPropagation 阻止了事件的继续传播,所以不会继续传播到冒泡阶段。

最后再看一段更有意思的代码:


<script>

 window.onload = function(){
 var outA = document.getElementById("outA"); 
 var outB = document.getElementById("outB"); 
 var outC = document.getElementById("outC"); 
 
 // 目标
 outC.addEventListener('click',function(event){alert("target");},false);

 // 事件冒泡
 outA.addEventListener('click',function(){alert("bubble");},false);

 // 事件捕获
 outA.addEventListener('click',function(){alert("capture");event.stopPropagation();},true); 
 
 };
 
</script>

<body>
 <div id="outA" style="width:400px; height:400px; background:#CDC9C9;position:relative;">
 <div id="outB" style="height:200; background:#0000ff;top:100px;position:relative;">
  <div id="outC" style="height:100px; background:#FFB90F;top:50px;position:relative;"></div> 
 </div>
 </div>
</body>



执行结果是只打印 capture,不会打印 target 和 bubble。神奇吧,我们点击了 outC,但是却没有触发 outC 上的事件处理函数,而是触发了 outA 上的事件处理函数。原因不做解释,如果你还不明白,可以再读一遍本文章。

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章