JS 事件机制是前端面试的 "硬核考点",从 DOM0 到 DOM2 的绑定区别,到捕获与冒泡的执行流程,再到事件委托的优化逻辑,面试官总能从里问到外,这篇文章带你吃透所有考点!
事件绑定:DOM0 和 DOM2 的区别,面试先问这个
给元素绑定事件,本质是告诉浏览器:"当某个动作(比如点击)发生时,执行我写的函数"。但绑定方式有两种,各有各的特点,面试官最爱问 "这俩有啥区别"。
(1)DOM0 级事件:简单但有局限的 "老式绑定"
直接通过on+事件名
绑定,有两种写法:
html
<!-- 写法1:HTML与JS耦合(不推荐) -->
<button onclick="handleClick()">点击</button>
<script>
function handleClick() {
console.log('点击事件触发');
}
</script>
<!-- 写法2:JS中直接赋值(稍好但仍有局限) -->
<script>
const btn = document.querySelector('button');
btn.onclick = function() {
console.log('DOM0:点击触发');
}
</script>
核心特点 :
- 同一事件只能绑定一个函数:如果再写
btn.onclick = 新函数
,旧函数会被直接覆盖(比如先绑了fn1
,再绑fn2
,触发时只执行fn2
)。 - 只能在冒泡阶段触发:没有控制 "捕获 / 冒泡" 的能力,事件触发时机固定。
- 耦合性高:写法 1 中 HTML 和 JS 混在一起,改逻辑时要同时动两个地方,维护麻烦(违反 "HTML 管结构、JS 管逻辑" 的分离原则)。
DOM2 级事件:更灵活的 "现代绑定"
用addEventListener
方法绑定,是现在的主流写法:
js
const btn = document.querySelector('button');
// 绑定点击事件
btn.addEventListener('click', function() {
console.log('DOM2:点击触发');
}, false); // 第三个参数控制阶段,默认false
参数说明:


- 第一个参数:事件类型(如
'click'
、'input'
)。 - 第二个参数:事件触发时执行的回调函数(监听器)。
- 第三个参数
useCapture
:布尔值,true
表示事件在 "捕获阶段" 触发,false
(默认)表示在 "冒泡阶段" 触发(核心考点,后面详说)。
核心优势
-
同一事件可绑定多个函数:比如再绑一个
click
事件,两个函数会按绑定顺序执行(不会覆盖)。javascriptbtn.addEventListener('click', () => console.log('函数1'), false); btn.addEventListener('click', () => console.log('函数2'), false); // 点击后输出:函数1 → 函数2
-
支持控制事件阶段:通过
useCapture
参数,可指定事件在 "捕获阶段" 还是 "冒泡阶段" 触发(DOM0 做不到)。 -
解耦 HTML 和 JS:事件逻辑全在 JS 中,HTML 只负责结构,改代码时不用两头找。
(3)冷知识:为啥没有 DOM1 级事件?
很多人疑惑 "DOM0、DOM2 都有,DOM1 去哪了?"------ 其实 DOM 规范的版本迭代里,DOM1(1998 年)只定义了基础的 DOM 操作接口 (比如getElementById
、appendChild
),压根没涉及事件机制。事件相关的规范是到 DOM2(2000 年)才正式加入的,这也是 "DOM2 级事件" 名字的由来。面试提一句,能体现你对规范的了解。
事件流:捕获→目标→冒泡
当你点击一个元素时,事件不是只在这个元素上触发就结束,而是会经历 "捕获→目标→冒泡" 三个阶段,这就是 "事件流"。理解这个流程,才能说清 "父元素和子元素的事件谁先执行"。
(1)三个阶段的具体流程
假设页面结构是:document → html → body → 父元素 → 子元素
(子元素是点击目标),事件流会按以下顺序执行:
- 捕获阶段 :从最顶层的
document
开始,逐层向下 "查找" 目标元素,依次触发绑定了useCapture=true
的事件。
路径:document → html → body → 父元素 → 子元素
(如果父 / 子元素的事件绑了useCapture=true
,会在这一步触发)。 - 目标阶段 :事件到达实际点击的元素(子元素),触发该元素的事件(不管
useCapture
是true
还是false
)。 - 冒泡阶段 :从目标元素开始,逐层向上 "返回" 到
document
,依次触发绑定了useCapture=false
(默认)的事件。
路径:子元素 → 父元素 → body → html → document
(如果父 / 子元素的事件绑了useCapture=false
,会在这一步触发)。
(2)代码示例:执行顺序一目了然
html
<div id="parent" style="padding: 50px; background: #eee;">
父元素
<div id="child" style="padding: 20px; background: #ccc;">子元素</div>
</div>
<script>
// 父元素绑定事件,useCapture=true(捕获阶段触发)
document.getElementById('parent').addEventListener('click', () => {
console.log('父元素:捕获阶段');
}, true);
// 子元素绑定事件,useCapture=true(捕获阶段触发)
document.getElementById('child').addEventListener('click', () => {
console.log('子元素:捕获阶段');
}, true);
// 父元素绑定事件,useCapture=false(冒泡阶段触发)
document.getElementById('parent').addEventListener('click', () => {
console.log('父元素:冒泡阶段');
}, false);
// 子元素绑定事件,useCapture=false(冒泡阶段触发)
document.getElementById('child').addEventListener('click', () => {
console.log('子元素:冒泡阶段');
}, false);
</script>
点击子元素后,输出顺序是:

(3)关键结论:useCapture
决定触发阶段
- 绑了
useCapture=true
的事件,在捕获阶段触发(从上到下)。 - 绑了
useCapture=false
(默认)的事件,在冒泡阶段 触发(从下到上)。
面试时被问 "父元素和子元素的事件执行顺序",直接按这个规则推导即可。
事件委托:优化性能的实战技巧,面试必问优势
如果有 100 个列表项需要绑定点击事件,不用给每个项都绑一次,而是把事件绑在它们的父元素上,利用 "冒泡阶段" 统一处理 ------ 这就是 "事件委托"。
(1)核心代码:3 行实现事件委托
html
<ul id="myList">
<li>item1</li>
<li>item2</li>
<li>item3</li>
<!-- 可能有更多li -->
</ul>
<script>
// 事件绑在父元素ul上,利用冒泡阶段触发
document.getElementById('myList').addEventListener('click', (event) => {
// 通过event.target判断实际点击的是哪个子元素(li)
if (event.target.tagName === 'LI') {
console.log('点击了:', event.target.textContent);
}
}, false);
</script>
原理 :当点击li
时,事件会冒泡到父元素ul
,ul
的事件处理函数通过event.target
(指向实际点击的li
)判断目标,从而执行对应逻辑。

(2)事件委托的 3 个核心优势
- 减少事件绑定次数,优化性能 :100 个
li
只需绑 1 次事件,而不是 100 次,减少浏览器内存占用(尤其在列表项极多的场景,性能提升明显)。 - 支持动态新增元素 :如果后续通过 JS 动态添加新的
li
(比如从后端加载数据),新元素不用重新绑定事件,父元素的监听器会自动处理(比 "新增一个绑一次" 简洁太多)。 - 集中管理事件逻辑:所有子元素的事件处理都在父元素的一个函数里,不用分散在多个地方,维护更方便。
避坑指南:这些细节容易答错
-
DOM0 和 DOM2 混用会出问题 :
如果同时用
onclick
(DOM0)和addEventListener
(DOM2)绑定同一事件,DOM0 会覆盖 DOM2 的事件:javascriptconst btn = document.querySelector('button'); btn.addEventListener('click', () => console.log('DOM2'), false); btn.onclick = () => console.log('DOM0'); // DOM0会覆盖DOM2 // 点击后只输出:DOM0
结论:统一用 DOM2 的
addEventListener
,避免混用。 -
event.target
和this
的区别:event.target
:指向 "实际触发事件的元素"(比如事件委托中,指被点击的li
)。this
:指向 "绑定事件的元素"(比如事件委托中,指父元素ul
)。
-
不是所有事件都能冒泡 :
大部分事件(如
click
、input
)会冒泡,但少数事件(如focus
、blur
)不会,使用时要注意。