标准答案

  1. 事件传播阶段:DOM 事件通常经历捕获阶段、目标阶段和冒泡阶段。捕获是事件从 window、document 向目标元素传递;冒泡是事件从目标元素再向外层祖先元素传递。addEventListener 的第三个参数或 options.capture 可以控制监听器是否在捕获阶段触发。
  2. 事件委托:事件委托是把监听器绑定在父元素上,利用事件冒泡和 event.target 判断真正触发的子元素。它适合列表、动态元素和大量相似交互,可以减少监听器数量,也能处理后续插入的子节点。
  3. stopPropagation 和 preventDefault:stopPropagation 用来阻止事件继续传播,preventDefault 用来阻止浏览器默认行为,例如链接跳转、表单提交、复选框默认切换。它们解决的问题不同,可以同时使用,但不能互相替代。
  4. 工程细节:事件委托时不要直接相信 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 阶段,导致判断复杂交互时顺序说不清。

作者信息