BRUCE BLOG / INTERACTION

抬起去哪了:一次点击丢失排查

轻点失灵、用力按一下又恢复。沿事件链逐层检查后,我们发现问题发生在网页收到事件之前,恢复策略却决定了后续体验。

杨永安 + Claude Code · 发布于 · 原始记录

一次点击,少了一半

反馈很像一个前端问题:设置侧边栏轻点时有些栏目没有反应,用力按一下触摸板,后面的轻点又正常了。早先我们曾把类似现象归因于 React 的点击兼容性,并把一些操作提前到按下时执行。但那个解释没有经过完整测量。

这次直接在实际窗口记录事件。失败时有 pointerdown 和 mousedown,随后没有抬起。下一次操作又少了新的 pointerdown。WebKit 仍以为上一轮按键没有松开,后续判断也跟着错了。问题从“点击不灵”变成了“释放事件在哪里丢了”。

pointerdown → mousedown → …
下一次:mousedown → pointerup → mouseup → click

沿着事件链,一层一层问

我们先确认命中的确实是按钮,没有遮罩或拖拽区域挡住它。然后检查 WebView、窗口、应用,以及系统投递给进程的事件。那个丢掉的 mouseUp 在这些层里都没有出现,连系统查询仍把左键记作按下。继续调整页面处理函数,已经无法解释这一段事实。

当时测试机上的窗口管理工具 Magnet 有一个监听鼠标按下、抬起的主动事件拦截器。退出它再复测,结果发生了变化:记录中的两组八次点击分别丢了两到三次抬起;退出后,只使用有效时长的十二次点击没有再丢失。这是当时设备与工具状态下的对照,不能外推为所有安装环境的结论。

合成事件也必须有对照。按下和抬起同一瞬间发出的 0ms 测试,在最小原生窗口中也会丢失,不能拿它代表真实轻点。仪器自己的异常,差点成了我们的产品结论。

同样的缺失,也可能有不同的体感

接下来,我们并排比较 Chromium 与 WKWebView 收到的同一类事件。两边都可能收到不完整的序列,差别在于丢失之后如何恢复。测量中,Chromium 会在下一次按下前先结束旧的按下状态;WebKit 则可能继续沿用错误状态。

标题栏归谁处理也重要:原生标题栏上的点击不一定进入网页,而 Bruce 的网页拖拽区域会让 WebView 参与。同样一句“Electron 手感好”,背后可能是恢复策略与事件边界的差异,不能直接简化成某个框架的点击函数更可靠。

修复状态,不能制造新的意图

Bruce 无法替别的程序修复事件拦截器,但可以在自己的 WebView 入口维护按下与抬起的配对。实现会结合系统按键变化、事件队列和下一次真实按下,判断何时需要修复。

补发的位置和时间一样重要。早期且目标仍可信时,可以落回按下处完成轻点;太迟的补发只复位,不执行用户已经放弃的操作。把抬起放到指针当前所在的新目标上,可能让共同祖先收到一次幽灵点击。

后续实现还区分了指针拖动与原生拖拽会话,并跟踪迟到的真实释放,避免一次动作结算两次。修复一个丢事件的问题,不应该靠向所有目标无差别地合成点击。

把排查方法留给下一位开发者

这些记录能帮助使用 WebView、原生窗口或混合界面的开发者,也能给代为排查的 Agent 一条更稳妥的路线。具体修复会随平台与版本变化,但可观测性和对照的方法可以复用。

  • 把“没响应”拆成命中、按下、抬起、派发与状态恢复。
  • 逐层确认事件是否到达,越过已排除的边界后就停止反复猜测。
  • 保留失败与成功两组对照,并注明合成事件的条件。
  • 仪器先确认目标窗口,避免合成输入落到别的应用。
  • 恢复过期状态时,不补做用户已经放弃的意图。

下载 Markdown