返回博客

微信小程序事件委托:e.target 还是 e.currentTarget?一字之差导致筛选栏间歇失效

在一个小程序的列表页里,我做了一排筛选 tab(全部 / 待处理 / 已完成),用了典型的事件委托写法:父 <view> 挂一个 bindtap,三个子 <view> 各自带 data-filter。代码看起来干净利落,但上线后用户反映:点筛选有时候没反应,有时候又好的——间歇性 bug,最难追。

现象

用手指点 tab 文字正中间 → 筛选不触发;点 tab 的边缘空白 → 筛选正常切换。必现复现路径:从正中间点"全部"两字,console 打出 filter = undefinedsetData 不执行。

根因

微信小程序事件对象有两个容易混淆的字段:

| 字段 | 指向谁 | dataset 来源 | |------|--------|-------------| | e.target | 手指真实落点的最内层节点(冒泡起点) | 该最内层节点的 data-* | | e.currentTarget | 绑定 bindtap 的那个节点(handler 挂的位置) | handler 节点的 data-* |

问题就出在这里。我的 WXML 结构是:

<view class="tab-bar" bindtap="changeFilter">
  <view class="tab-item" data-filter="all">全部(12)</view>
  <view class="tab-item" data-filter="pending">待处理(5)</view>
  <view class="tab-item" data-filter="done">已完成(7)</view>
</view>
  • 点 tab-item 边缘空白e.target = tab-item → dataset.filter = "all"
  • 点 tab-item 内部文字 → 文字节点被编译为内部 text 盒子,e.target 指向它 → dataset.filter = undefined

而 handler 里我写的是 e.currentTarget.dataset.filter——currentTarget 永远指向父级 tab-bar,父级根本没有 data-filter,结果同样是 undefined

坑:委托模式下混用了两种字段语义

父级 bindtap + 子级 data-*委托模式,此时 currentTarget 指向没有 data-* 的父节点,永远拿不到值。正确做法是用 e.target.dataset——但 target 会在子节点内部还有嵌套(text/icon)时再次失效。两种姿势各有适用场景,搞混就是间歇 bug。

正解

模式 A(推荐):每个子节点独立绑定,用 currentTarget

<view class="tab-bar">
  <view class="tab-item" bindtap="changeFilter" data-filter="all">全部</view>
  <view class="tab-item" bindtap="changeFilter" data-filter="pending">待处理</view>
  <view class="tab-item" bindtap="changeFilter" data-filter="done">已完成</view>
</view>
changeFilter(e) {
  const filter = e.currentTarget.dataset.filter; // ✅ bindtap 挂在 tab-item,currentTarget 就是它
  if (filter && filter !== this.data.filter) {
    this.setData({ filter });
  }
}

点击 tab-item 内部任何节点,事件都会冒泡到 tab-itemcurrentTarget 始终是它,dataset 稳定有值。

模式 B(简洁但脆弱):父级委托 + 用 e.target,加防御早退

changeFilter(e) {
  const filter = e.target.dataset.filter; // 命中 tab-item 时有值,命中内部文字时 undefined
  if (!filter) return;                    // ✅ 必须加这个早退,否则空值 setData 污染状态
  if (filter !== this.data.filter) {
    this.setData({ filter });
  }
}

模式 B 只适合子项是纯文本、无嵌套子节点的情形。一旦 tab 里加了 icon 或 <text> 包裹,命中内层节点时 e.target 就不再是 tab-item,又回到 undefined。可以在子节点上加 CSS pointer-events: none 强制让点击穿透到 tab-item,但这比较 hacky。

正解:决策表

| tab-item 内部是否有嵌套 text/icon | 推荐模式 | |----------------------------------|---------| | 纯文本,无嵌套 | B(父委托 + e.target + if (!filter) return) | | 含 icon 或 <text> 包裹 | A(子各自 bindtap + e.currentTarget | | wx:for 动态生成 | A,避免内部 DOM 演化引发漏 data |

同一文件里多个委托 handler 必须风格一致,复制粘贴时把模式 A 的 currentTarget 套到模式 B 的结构上,就是这次 bug 的直接原因。

排查 Checklist

  • [ ] bindtap 挂在哪个节点?data-* 挂在哪个节点?两者是否同一节点?
  • [ ] handler 里用的是 e.target 还是 e.currentTarget?与上一条匹配吗?
  • [ ] tab-item 内部是否有 <text> / <image> / icon?命中它们时 dataset 还有值吗?
  • [ ] 是否写了 if (!filter) return 防御父级空白区命中?

一句话外卖

currentTarget 永远跟着 bindtap 走,target 永远跟着手指走——委托模式的 data-* 挂在哪里,就用对应那侧的字段。