标准答案
- 事件传播阶段:DOM 事件通常经历捕获阶段、目标阶段和冒泡阶段。捕获是事件从 window、document 向目标元素传递;冒泡是事件从目标元素再向外层祖先元素传递。addEventListener 的第三个参数或 options.capture 可以控制监听器是否在捕获阶段触发。
- 事件委托:事件委托是把监听器绑定在父元素上,利用事件冒泡和 event.target 判断真正触发的子元素。它适合列表、动态元素和大量相似交互,可以减少监听器数量,也能处理后续插入的子节点。
- stopPropagation 和 preventDefault:stopPropagation 用来阻止事件继续传播,preventDefault 用来阻止浏览器默认行为,例如链接跳转、表单提交、复选框默认切换。它们解决的问题不同,可以同时使用,但不能互相替代。
- 工程细节:事件委托时不要直接相信 event.target,目标可能是图标、文字节点或按钮内部元素。更稳妥的写法是用 closest 或 matches 找到符合选择器的目标,并确认目标仍在当前容器内。
题目解析
事件传播模型和工程用法要分开理解。捕获、目标、冒泡是事件流的三个阶段;事件委托只是利用冒泡阶段把子元素事件交给父元素统一处理。
stopPropagation 和 preventDefault 是两个完全不同的控制点。前者控制事件还要不要继续传播,后者控制浏览器默认行为还要不要执行。比如点击 a 标签时阻止跳转要用 preventDefault,不是 stopPropagation。
事件委托的价值不只是“少绑定几个监听器”,还包括支持动态新增节点。列表、菜单、表格操作按钮这类场景尤其适合。
实际代码里 target 可能是按钮里的图标或文字节点,所以需要 closest/matches 做目标归一化,并校验目标仍在当前容器内,避免嵌套结构或外部节点导致误触发。
代码示例
下面例子能说明捕获监听和事件委托的典型写法。
JavaScript
document.addEventListener('click', () => {
console.log('capture')
}, { capture: true })
const list = document.querySelector('.todo-list')
list.addEventListener('click', event => {
const button = event.target.closest('[data-action="remove"]')
if (!button || !list.contains(button)) {
return
}
event.preventDefault()
button.closest('li')?.remove()
})常见误区
- 把阻止冒泡和阻止默认行为混为一谈。
- 事件委托时不做目标过滤,导致点击父容器空白处也触发逻辑。
- 忽略 capture 阶段,导致判断复杂交互时顺序说不清。