想象你网购了一箱水果。快递从城市配送中心出发,途经区级分拨站、街道驿站、小区快递柜,最终由骑手递到你手上。你签收后,系统里会有一条回执记录,逐级传回配送中心完成闭环。网页里每一次点击,上演的也是同样的“派送剧”:事件像包裹一样从 window 顶层逐级下沉到你点击的那个元素(捕获阶段),在这里“命中目标”(目标阶段),再逐级上浮回到顶层(冒泡阶段)。而“半路拦截”“拒绝签收”“请物业代收”这些生活里的操作,恰好对应着 JavaScript 中 stopPropagation、preventDefault 和事件委托。这篇文章就用这套“快递逻辑”,带你一次看清事件模型的全部设计。

岗位关联

对 AI 从业者的价值:事件模型是所有前端交互的底层协议,无论是给 AI 应用写对话界面、做可视化大屏,还是部署行为埋点,都离不开它。懂了它,你的交互代码更健壮,排查问题快人一步。

目录
  1. 可视化演示
  2. 事件流三阶段
  3. 冒泡与捕获
  4. 阻止事件行为
  5. 事件委托
  6. 自定义事件
  7. 事件对象
  8. 常见踩坑
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

事件流三阶段:捕获、目标与冒泡 事件捕获阶段:从window向下传递到目标元素的祖先节点,依次触发捕获监听器 事件冒泡阶段:从目标元素向上传播到window,是大多数事件的默认行为,事件委托基于此机制 stopPropagation阻止事件继续传播,preventDefault阻止默认行为如链接跳转 事件流 = 捕获阶段 → 目标阶段 → 冒泡阶段 事件委托利用冒泡在父元素统一处理子元素事件

事件流三阶段

快递派送的完整旅程其实分成三段:包裹从城市配送中心出发,经各区分拨站一路下沉到你所在的小区;骑手把包裹送到你手上;你签收后,回执信息又逐级传回配送中心。这三段缺一不可,构成了快递配送的完整闭环。浏览器的事件,走的也是这条“三段式”路线。

当你点击页面上一个按钮,浏览器不会把“点击”这件事直接丢给按钮,而是从最顶层的 window 对象出发,沿着 DOM 树逐级下沉,经过 document、html、body 等祖先节点,最终到达你点中的元素。这条下沉链路叫捕获阶段;到达目标元素的那一瞬间叫目标阶段;随后事件又调头向上、沿原路逐级传回 window,这条上浮链路叫冒泡阶段。

完整的事件流路径是:window → document → html → body → …… → 目标元素 → …… → body → html → document → window。监听事件最常用的 addEventListener 默认在冒泡阶段触发,因为此时事件已经从目标元素“回传”到了监听器所在的祖先节点;想在捕获阶段提前“截取”事件,就把第三个参数设为 true。

回到快递的比喻:捕获阶段像配送中心逐级发货,目标阶段像骑手当面签收,冒泡阶段像回执单逐级传回。浏览器里的每一次点击,都严格走完这三段旅程,无论中间隔着多少层“中转站”。

window→捕获target→冒泡window\displaystyle \text{window} \xrightarrow{\text{捕获}} \text{target} \xrightarrow{\text{冒泡}} \text{window}
一句话看懂:事件先自上而下“送货”,命中目标后再自下而上“回执”,完整路径构成一个 U 形。
  1. 下沉的捕获段window 到 target 之间逐级下潜的链路,任何一层都能提前监听
    window→捕获target\displaystyle \text{window} \xrightarrow{\text{捕获}} \text{target}
  2. 命中的目标点事件真正触发的那个元素,位于链路的最末端
    target\displaystyle \text{target}
  3. 上浮的冒泡段target 回到 window 的逐级回传链路,默认监听都在这一段生效
    target→冒泡window\displaystyle \text{target} \xrightarrow{\text{冒泡}} \text{window}

冒泡与捕获

为什么快递回执要一级一级往回传,而不是骑手直接打电话给配送中心?因为每一级中转站都需要留个记录——包裹经过谁、谁经手的,出了问题才知道去哪查。冒泡正是这样:事件一层层往上走,让沿途每个祖先节点都有机会“看一眼”。

捕获与冒泡是一对方向相反的传播过程。捕获是“自上而下”:从 window 到目标元素,像快递发货时的逐级下拨;冒泡是“自下而上”:从目标元素回到 window,像回执单的逐级回传。两者会经过同一批祖先节点,只是顺序完全相反。

用 addEventListener 监听时,第三个参数是决定方向的开关:false(默认)表示在冒泡阶段触发,true 表示在捕获阶段触发。同一个祖先元素如果同时注册了 true 和 false 两个监听器,捕获阶段先执行前者,冒泡阶段再执行后者。要注意:目标阶段本身没有“捕获/冒泡”之分,不管参数是什么,目标元素上的监听器都会在事件到达时触发。

拿着回执单逐级登记是冒泡,拿着派送单逐级下拨是捕获。方向相反,目的相同——让每一级祖先都有机会响应。理解这对“顺水”与“逆水”的关系,你就拿到了阅读事件传播的第一把钥匙。

addEventListener(type, handler, capture)\displaystyle \text{addEventListener}(\text{type},\ \text{handler},\ \text{capture})
一句话看懂:第三个参数不决定“冒不冒泡”,只决定监听器“站在哪一段路上”。
  1. 事件类型type 是事件名,如 click、keydown,决定监听什么信号
    type∈{click, keydown, …}\displaystyle \text{type} \in \{\text{click},\ \text{keydown},\ \ldots\}
  2. 处理函数handler 是事件触发时要执行的函数,事件对象作为参数传入
    handler(event)\displaystyle \text{handler}(\text{event})
  3. 阶段开关capture 为 true 时监听器在捕获段触发,false 时在冒泡段触发
    capture∈{true, false}\displaystyle \text{capture} \in \{\text{true},\ \text{false}\}

阻止事件行为

快递路上可能发生两种“意外”:一是某级中转站发现包裹不该继续往下派,直接拦下退回;二是收件人坚决拒收,包裹被原路退回。前者是“拦截”,后者是“拒收”,看起来都让包裹停了,但原因完全不同。事件模型里恰好有两个 API 对应这两种情况。

stopPropagation 负责“拦截”:调用后,事件立即停止继续传播,后面无论还有多少层祖先,都再也收不到它。preventDefault 负责“拒收”:它不管事件传不传,只是阻止浏览器对这个事件的默认动作——比如链接的跳转、表单的提交、复选框的勾选。

两者可以同时调用,互不影响:stopPropagation 管的是“事件这条路还走不走”,preventDefault 管的是“浏览器固有的行为做不做”。典型场景:表单校验不通过时用 preventDefault 阻止提交,同时用 stopPropagation 防止事件冒泡到 document 触发全局逻辑。注意,老代码里的 return false 在不同场景下行为并不一致,现代推荐显式调用这两个 API。

中转站拦截的是“包裹的行程”,收件人拒收的是“签收动作本身”。记住这组对照,面试中再被问“二者区别”,你就能四两拨千斤。

stopPropagation⇒传播终止;preventDefault⇒默认行为取消\displaystyle \text{stopPropagation} \Rightarrow \text{传播终止};\quad \text{preventDefault} \Rightarrow \text{默认行为取消}
一句话看懂:一个管“路的长度”,一个管“路的尽头那扇门开不开”。
  1. 拦截传播stopPropagation 切断事件剩余传播路径,后续祖先不再收到
    event.stopPropagation()\displaystyle \text{event}.\text{stopPropagation}()
  2. 取消默认preventDefault 让浏览器跳过固有动作,但传播不受影响
    event.preventDefault()\displaystyle \text{event}.\text{preventDefault}()

事件委托

整栋楼每天几十个快递,如果每家每户门口都雇一个签收员,成本高得离谱。物业想了个聪明的办法:在小区门口设一个统一代收点,由物业统一签收,再根据每家每户的门牌号分发。新搬来的住户不用安装任何设备,自动就能享受代收服务。

事件委托就是这种“物业代收”思想。与其给每个子元素都绑一个监听器,不如把监听器统一绑在它们共同的祖先上。当子元素被点击,事件冒泡到祖先时,祖先的监听器统一处理,再通过 event.target 判断“到底是哪家住户的件”。

事件委托的两个核心收益:一是监听器数量从 N 个降到 1 个,内存占用和初始化开销大幅减少;二是对动态增删的元素天然友好——新增的子元素不需要重新绑定,因为处理逻辑在祖先上。判断具体元素时,用 e.target 检查 tagName 或 classList 即可。经典实战:给 ul 绑定点击监听,用 e.target 判断是否点中了某个 li,再执行对应操作。

物业代收点不会替每户装新设备,祖先监听器也不会为每个新节点绑新函数。这套“找人代收”的智慧,正是事件委托在大规模列表、动态渲染场景里长盛不衰的原因。

1 listener(parent)→ e.target childactual\displaystyle 1\ \text{listener(parent)} \xrightarrow{\ e.\text{target}\ } \text{child}_{\text{actual}}
一句话看懂:一个父级监听器,通过 target 精确分发到真正的子级触发者。
  1. 祖先监听父级用一个监听器代理所有子元素的同类事件
    parent.addListener(click, fn)\displaystyle \text{parent}.\text{addListener}(\text{click},\ fn)
  2. 目标定位通过 e.target 找到真正被点击的子元素,完成精确分发
    e.target=childactual\displaystyle e.\text{target} = \text{child}_{\text{actual}}

自定义事件

快递系统只管“送货—签收”这一件事,可小区居民想搞读书会、跳蚤市场,这些不在快递业务范围内。怎么办?大家自己约定暗号:“谁想参加,就在楼下公告栏贴一张红色纸条。”这套自创的流程,不经过任何官方中转站。

自定义事件就是前端世界的“自创暗号”。浏览器内置了 click、scroll 等事件,但业务中经常需要“加载完成”“数据更新”“用户登录”这些语义化信号。用 new CustomEvent 创建事件,用 dispatchEvent 分发出去,再用 addEventListener 监听——完全复用事件模型一套体系,表达的却是你自己的业务语义。

创建自定义事件推荐 new CustomEvent(type, { detail: data }),detail 可以携带任意业务数据,监听器里通过 e.detail 读取。想让自定义事件也能冒泡、被委托,就在配置里加 bubbles: true。dispatchEvent 是同步调用的,监听器会在这一行执行完毕后才继续往下走。自定义事件还被广泛用于组件间解耦通信——两个组件不直接引用彼此,而是通过一个公共事件通道对话。

红色纸条是居民自创的“事件”,公告栏是“事件通道”,会议组织者是“监听器”。浏览器允许你完全复刻这套机制,让你的应用拥有和原生事件一模一样的传播能力。

new CustomEvent(type, {detail:data})→ dispatchEvent listener\displaystyle \text{new CustomEvent}(\text{type},\ \{\text{detail}: \text{data}\}) \xrightarrow{\ \text{dispatchEvent}\ } \text{listener}
一句话看懂:造一个事件、发一条消息、收一次广播,三段式走完整套传播流程。
  1. 造事件CustomEvent 构造器生成一个附带业务数据的事件对象
    new CustomEvent(type, {detail:data})\displaystyle \text{new CustomEvent}(\text{type},\ \{\text{detail}: \text{data}\})
  2. 分发起爆dispatchEvent 让事件正式进入传播流程,沿途监听器依次执行
    node.dispatchEvent(event)\displaystyle \text{node}.\text{dispatchEvent}(\text{event})
  3. 监听接收addEventListener 按事件类型接收并处理这份“自创信号”
    node.addEventListener(type, handler)\displaystyle \text{node}.\text{addEventListener}(\text{type},\ \text{handler})

事件对象

每个快递包裹上都贴着快递单:收件人、寄件人、物品、运单号。千万别把“收件人”和“签收点”搞混——包裹上写的是收件人李四,但签收点是小区物业,这两个信息不是一回事。事件对象 event 就是事件的“快递单”。

监听器触发时会自动收到一个 event 参数,上面记录着各种信息:事件类型、触发时间、按键信息等。最容易被混淆的是两个“人”:event.target 是“收件人”——真正触发事件的那个元素;event.currentTarget 是“签收点”——当前正在执行监听器、也就是绑定监听的元素。

在事件委托场景里,监听器绑定在 ul 上、实际点中的是某个 li,此时 e.target 是 li,e.currentTarget 是 ul——两者必然不同。而在非委托、直接在元素上绑监听的场景,两者相同。理解这个区别,调试委托代码时就不会“找不到是哪个元素触发的”。其他常用属性还有 event.type(事件类型)、event.bubbles(是否可冒泡)、event.defaultPrevented(默认行为是否已被阻止)、event.timestamp(触发时间戳)。

看快递单时先分清“寄给谁”和“谁签收”;处理事件时先分清 e.target 和 e.currentTarget。这一份“快递单”上的每个字段,都是你排查交互问题时的线索。

e.target≠e.currentTarget(事件委托场景)\displaystyle e.\text{target} \neq e.\text{currentTarget} \quad (\text{事件委托场景})
一句话看懂:target 是谁发起的,currentTarget 是谁在处理,委托场景下两者必然不同。
  1. 实际触发者e.target 指向真正产生事件的元素,不随监听位置改变
    e.target=实际被点击元素\displaystyle e.\text{target} = \text{实际被点击元素}
  2. 当前处理者e.currentTarget 指向正在执行监听器的元素,等于绑定监听的元素
    e.currentTarget=绑定监听的元素\displaystyle e.\text{currentTarget} = \text{绑定监听的元素}

常见踩坑

快递再熟悉也有翻车的时刻:给已搬走的住户门口堆了一堆无人取的件(监听器泄露);把整栋楼的快递全扔在物业结果没人认领(this 指向错乱);回执单本身也要走全楼流程,结果每家都以为要自己处理一遍(冒泡干扰)。

事件模型的常见坑集中在这几类:一是内存泄漏——监听器绑定后没有在组件销毁时移除,反复创建销毁组件时监听器越积越多;二是冒泡干扰——子元素和祖先元素都绑了同一事件的监听器,点击子元素却触发了祖先的逻辑;三是 this 指向错乱——普通函数里 this 指向 currentTarget,换成箭头函数后 this 又变成外部作用域,判断错了整个逻辑就变了味。

具体的止血方案:removeEventListener 必须持有原函数引用,addEventListener 和 removeEventListener 要成对出现,匿名函数无法被移除;在顶层节点用 stopPropagation 拦截“多余”的冒泡,或改造结构避免嵌套监听;普通函数里不要依赖 this 判断元素,直接用 e.currentTarget。还有一个隐性坑:同一元素重复注册同名事件,触发时会依次执行,不要误以为“只执行一次”。

快递系统的翻车,总能从“件没送到该到的人手里”归结出原因;事件模型的坑,也总能从“监听器绑了没解、冒泡跑错了层、this 看错了对象”这三条主线里找到答案。

addEventListener(type,f)⟺removeEventListener(type,f)\displaystyle \text{addEventListener}(\text{type}, f) \Longleftrightarrow \text{removeEventListener}(\text{type}, f)
一句话看懂:绑定和解绑必须用同一个函数引用,就像取快递必须凭同一张单号。
  1. 成对注册绑定监听时记录下函数引用,为将来解绑做好准备
    addEventListener(type,f)\displaystyle \text{addEventListener}(\text{type}, f)
  2. 成对解绑移除时必须传入同一个引用 f,匿名函数因引用无法匹配而无从移除
    removeEventListener(type,f)\displaystyle \text{removeEventListener}(\text{type}, f)

AI 应用场景

AI 对话界面的流式消息交互

阶段:前端交互层 / UI 事件处理

解决的问题:AI 聊天消息是流式、动态生成的,每条消息的“复制”“重新生成”“点赞”按钮都是新插入的 DOM。传统逐个绑定事件,新消息出来要手动补绑定,代码冗余且容易漏绑。用事件委托后,消息列表容器统一监听点击,用 e.target 判断点中了哪个功能按钮,任何新增消息自动获得全部交互能力。

具体案例:某 LLM 对话产品把“复制回答”按钮的监听从每条消息移到消息列表容器,用 data-action="copy" 属性区分动作,新消息无需任何额外注册,上线后动态按钮的交互报错率降为零。

AI 数据大屏的用户行为埋点

阶段:数据采集与用户行为分析

解决的问题:AI 可视化大屏需要统计用户点击了哪些图表、切换了哪些视图。传统做法在每张图表上分别绑监听,埋点代码与业务代码耦合严重。用自定义事件统一封装“视图切换”“筛选变化”等业务语义,业务组件只管 dispatchEvent,顶层的分析脚本统一监听上报,埋点集中在一处。

具体案例:某 AI 运营大屏项目里,图表筛选变化时组件 dispatch 一个 viewChange 自定义事件,顶层脚本统一监听并上报到日志系统,新增图表时无需再改埋点代码。

AI 推荐信息流的懒加载卡片

阶段:动态内容渲染与交互增强

解决的问题:推荐系统基于用户行为不断加载新卡片,每张卡片都有点赞、收藏、不喜欢等按钮。如果每次都重新绑定监听器,卡片一多内存暴涨、滚动卡顿。事件委托让列表容器统一响应,配合 e.target 判断具体操作,性能与扩展性兼得。

具体案例:某资讯 App 的 AI 推荐流,在一个滚动容器上绑定了全部卡片按钮的监听,通过 data-index 定位卡片 id,滚动 fps 平均提升 20%,新卡片加载完全无感。

Python 演示

python81 行
"""用 Python 模拟 JavaScript 事件模型的捕获-目标-冒泡传播过程""" class Event:    def __init__(self, etype, target):        self.type = etype        self.target = target        self.propagation_stopped = False        self.default_prevented = False     def stop_propagation(self):        self.propagation_stopped = True     def prevent_default(self):        self.default_prevented = True  class Node:    def __init__(self, name, parent=None):        self.name = name        self.parent = parent        self.capture_listeners = []        self.bubble_listeners = []     def add_listener(self, fn, use_capture=False):        if use_capture:            self.capture_listeners.append(fn)        else:            self.bubble_listeners.append(fn)     def dispatch(self, event):        path = []        node = self        while node:            path.append(node)            node = node.parent         for n in reversed(path):            if event.propagation_stopped:                break            for fn in n.capture_listeners:                fn(event, n)         for n in path:            if event.propagation_stopped:                break            for fn in n.bubble_listeners:                fn(event, n)  def make_handler(label):    def handler(event, current):        role = "目标" if current is event.target else "中间层"        print(f"[{label}] {current.name}({role}) 收到 click, 来源: {event.target.name}")    return handler  def main():    window = Node("window")    document = Node("document", window)    div = Node("div", document)    button = Node("button", div)     window.add_listener(make_handler("捕获"), use_capture=True)    window.add_listener(make_handler("冒泡"))    div.add_listener(make_handler("冒泡"))    button.add_listener(make_handler("自身"))     click = Event("click", target=button)    print("=== 模拟点击 button ===")    button.dispatch(click)     print("\n=== stopPropagation 演示: 在 div 处拦截 ===")    def intercept(event, current):        event.stop_propagation()        print("div 拦截了事件, 不再向上冒泡")    div.add_listener(intercept)    button.dispatch(Event("click", target=button))  if __name__ == "__main__":    main()
目的:用 Python 面向对象方式复刻事件传播三段流程,直观演示捕获、目标、冒泡的触发顺序,以及 stopPropagation 的中断效果。
  1. while node:从触发元素沿 parent 链一路向上收集整条传播路径
  2. for n in reversed(path):捕获阶段自上而下遍历路径节点
  3. if event.propagation_stopped:stopPropagation 被调用后传播立即中断
  4. for n in path:冒泡阶段自下而上遍历路径节点
  5. role = "目标" if current is event.target else "中间层"用 target 判断当前节点角色
  6. click = Event("click", target=button)构造事件对象,记录触发者
  7. button.dispatch(click)从目标元素发起完整的事件传播

常见误区

误区我以为调用了 `e.stopPropagation()` 之后,点击链接就不会跳转了,结果页面照跳不误,是不是我代码写得有问题?

正解stopPropagation 只负责停止事件在 DOM 树上的传播,链接跳转是浏览器对 click 事件的“默认行为”,由 preventDefault 管。就像快递“中途拦截”只是不让包裹继续走,收件人那边该做的签收动作根本不受影响。要阻止跳转,必须调用 e.preventDefault()。

为何常见:两个 API 名字都带“阻止”,初学者很容易把“阻止传播”和“阻止默认行为”混为一谈。

误区我一直以为 `addEventListener` 的第三个参数填 `true` 表示“开启冒泡”,填 `false` 表示“关闭冒泡”,这样理解应该没错吧?

正解冒泡是事件流的默认行为,不能靠这个参数关闭。第三个参数 true 表示监听器在捕获阶段触发,false(默认)表示在冒泡阶段触发,它管的是“监听器放在传播的哪一段”,而不是“要不要冒泡”。

为何常见:参数名 capture 容易被望文生义,加上一些教程随口说“true 捕获 false 冒泡”,听多了就记岔了。

误区我按教程把 click 监听绑到了 ul 上,结果点空白区域也触发逻辑,事件委托是不是有 bug?

正解事件委托不仅要“绑在父元素”,还要用 e.target 判断“到底点中了谁”。委托的本质是祖先代收、按 target 分发,少了 target 校验,任何冒泡上来的事件(包括点在 ul 空白处)都会触发,就像物业把没人签收的件全算在住户头上。

为何常见:只看到了“少绑监听”的表面收益,忽略了委托的另一半——目标识别。

误区我绑定时写的是 `addEventListener('click', function(){...})`,想在某个时机移除,但写了 `removeEventListener('click', function(){...})` 却没用。

正解removeEventListener 要求传入的函数引用必须和绑定时完全一致。两次写的匿名函数字面量是两个不同对象,引用不同,自然移除失败。正确做法:把函数命名为常量 const handler = () => {...},绑定和解绑都传它。就像取快递要凭同一张单号,重新抄一份可不对。

为何常见:函数在 JS 里是引用类型,初学者容易按“值相等”去理解,忽略了引用相等。

误区我以为子元素绑定 click 后,父元素就不应该再收到 click 了,结果发现父元素的监听也被触发,这不是冲突了吗?

正解这正是事件冒泡的设计本意。事件在目标元素触发后,会沿 DOM 树向上传播,沿途每个绑了监听的祖先都会依次收到。这如同快递回执逐级传回,每一级都能登记签收。如果不想要这种效果,就在子元素监听器里调用 e.stopPropagation()。

为何常见:把“事件绑定”直觉理解为“一对一独占”,忽略了事件是“一份信号沿链路传阅”的模型。

面试题

Q1. 在标准事件流中,事件到达目标元素之前先要经历哪个阶段?

单选题 · easy

  • A.冒泡阶段
  • B.捕获阶段← 正确答案
  • C.目标阶段
  • D.委托阶段

答案:B。事件流总是先捕获、再目标、最后冒泡。捕获阶段从 window 逐级下沉,找到目标之前先经历它;目标阶段是到达的瞬间;冒泡阶段在最末段;根本不存在“委托阶段”。就像快递派送,包裹先逐级下沉到收件人,签收后再逐级传回。

Q2. addEventListener('click', fn, true) 中的 true 表示什么?

单选题 · easy

  • A.在冒泡阶段触发监听
  • B.在捕获阶段触发监听← 正确答案
  • C.自动开启事件委托
  • D.立即阻止事件传播

答案:B。第三个参数 capture 为 true 时,监听器在捕获阶段触发;false(默认)在冒泡阶段触发。它决定监听器站在传播的哪一段,而不是“要不要冒泡”。像决定是在派送途中查验包裹,还是在回执途中登记。

Q3. 以下哪个方法能阻止浏览器对链接跳转的默认行为?

单选题 · mid

  • A.调用 stopPropagation 方法
  • B.调用 preventDefault 方法← 正确答案
  • C.调用 stopPropagation 后再返回
  • D.移除该元素的监听器

答案:B。链接跳转是浏览器对 click 的默认行为,只能用 preventDefault 取消,就像收件人拒收迫使包裹原路退回。stopPropagation 只拦传播不拦默认动作,返回语句更无关;移除监听器只会让事件无人处理,跳转照样发生。

Q4. 关于事件委托的说法,正确的有哪些?

多选题 · mid

  • A.减少监听器数量与内存占用← 正确答案
  • B.动态子元素需重新绑定监听
  • C.委托只能应用在列表场景
  • D.借助 e.target 识别触发元素← 正确答案

答案:AD。事件委托把监听器集中到祖先上,监听数量从 N 降到 1,节省内存(A 对);动态新增的子元素由祖先代收、无需重新绑定,B 说法正是委托要避免的问题(B 错);委托适用于各种容器结构,不限于列表(C 错);定位实际元素靠 e.target(D 对)。答案选 A、D。

Q5. 在事件委托场景中,e.target 和 e.currentTarget 的关系是?

单选题 · mid

  • A.两者总指向同一元素
  • B.target 为实际触发元素← 正确答案
  • C.currentTarget 为父级元素
  • D.二者指向最深目标节点

答案:B。委托场景里,target 是实际被点中的子元素,currentTarget 是正在执行监听的绑定元素,二者不同;只有非委托场景才相同。A 太绝对;C 不准确,currentTarget 等于绑定监听的元素,未必是父级;D 混淆了概念。就像包裹的收件人和签收点是两个信息。

Q6. 事件冒泡的方向是?

单选题 · easy

  • A.从 window 传到目标节点
  • B.从目标节点传到 window← 正确答案
  • C.只在目标元素内部传递
  • D.沿随机路径进行传播

答案:B。冒泡是从目标元素向 window 逐级向上传播,像签收回执逐级传回配送中心。A 是捕获方向;C 只在原地就没有传播;D 事件流路径固定双向,不会随机。

Q7. 下列哪些做法能避免事件模型相关的内存泄漏?

多选题 · mid

  • A.组件销毁时移除监听← 正确答案
  • B.使用命名函数绑定事件← 正确答案
  • C.监听器绑定后无需清理
  • D.所有事件都绑定在 document 上

答案:AB。避免内存泄漏要成对地绑定和解绑监听(A 对);命名函数才能保证移除时引用一致(B 对);“无需清理”是错误认知,反复创建销毁组件时监听器会越积越多(C 错);把事件全绑在 document 上会造成全局监听堆积、难以维护,不是正解(D 错)。答案选 A、B。

Q8. 某元素在捕获阶段和冒泡阶段都注册了同一事件的监听器,事件触发时执行顺序是?

单选题 · hard

  • A.先冒泡而后捕获执行
  • B.先捕获而后冒泡执行← 正确答案
  • C.只执行捕获监听器
  • D.只执行冒泡监听器

答案:B。捕获阶段在前、冒泡阶段在后,所以同一元素上捕获监听先执行、冒泡监听后执行。就像一件包裹派送途中被查验一次,回执途中又被登记一次,顺序是先下行后上行。

Q9. 关于 CustomEvent 的说法,正确的有?

多选题 · hard

  • A.可用 detail 携带业务数据← 正确答案
  • B.不设置 bubbles 也能冒泡
  • C.dispatchEvent 是同步执行的← 正确答案
  • D.自定义事件无法被委托捕获

答案:AC。CustomEvent 的 detail 属性专门携带业务数据(A 对);bubbles 必须显式设为 true 才能冒泡(B 错);dispatchEvent 同步调用,监听器当场执行完毕(C 对);设置了 bubbles: true 的自定义事件同样能被祖先委托捕获(D 错)。答案选 A、C。

Q10. 页面只有 window→div→button 三层。button 绑定捕获监听,div 绑定冒泡监听。点击 button 后,两个监听器的触发顺序是?

单选题 · extreme

  • A.先触发 button 的捕获监听← 正确答案
  • B.先触发 div 的冒泡监听
  • C.两个监听同时触发执行
  • D.只有 div 的监听会执行

答案:A。捕获阶段从 window 逐级下沉到 button,button 的捕获监听在目标处先触发;随后冒泡阶段从 button 逐级上浮,div 的冒泡监听才触发,顺序是先子后父。两个监听分属不同阶段串行执行而非同时(C 错);button 的捕获监听确实会执行(D 错)。