返回博客列表
2026年4月12日8 分钟阅读

滚轮滚轮测试工具是如何工作的?

你有没有好奇过,一个网页是怎么判断你的鼠标滚轮有没有问题的?下面我们来了解一下鼠标滚轮测试工具的实现原理。

你每次滚鼠标,程序是怎么"读懂"你的意图的?

有没有想过,你用手指轻轻拨动鼠标滚轮的那一下,在滚轮测试工具的眼里到底是什么样子的?

对我们人类来说,"滚一下鼠标"是一个完整的、有意义的动作。但对程序来说,它接收到的是一串冰冷的数字流:每隔几毫秒,操作系统就会报告一次 deltaY 值,告诉程序"滚轮动了多少"。这些数字一个接一个涌进来,程序根本不知道哪里是"一次滚动"的开始,哪里是结束,更不知道中间哪些数据是用户真实的操作意图,哪些是鼠标本身抖动产生的噪音。

这篇文章要聊的,就是一套用来解决这个问题的系统。它实现了两种核心算法:

  • 算法一:冲程边界检测 —— 负责把连续的数字流,切割成一段一段有意义的"滚动操作"
  • 算法二:抖动点识别 —— 负责在每段滚动操作里,找出哪些数据点是"噪音",不该被信任

我会尽量用最直白的语言来解释这两套算法,不需要你有任何编程基础,只要你用过鼠标就够了。


在开始之前的问题:什么是"冲程"?

代码里有一个很关键的词:Stroke(冲程)

这个词借用自机械工程。在发动机里,活塞从一端运动到另一端叫做一个"冲程"。这里借过来表示:用户完成的一次完整的、连续的滚动动作

想象你在看一篇很长的文章:

  1. 你拨动滚轮,页面往下滚了一段 —— 这是第一个冲程
  2. 你停下来读了几秒钟
  3. 你又拨动滚轮,页面继续往下滚 —— 这是第二个冲程
  4. 你觉得看过头了,往回滚了一点 —— 这是第三个冲程

每个冲程都是独立的、有意义的操作单元。把这些冲程识别出来,程序才能真正理解用户在干什么。


算法一:冲程边界检测

核心问题:我怎么知道你"停了"?

这是整个系统最难的部分。

难在哪里?难在用户滚动的"节奏"千变万化。有人喜欢快速猛滚,有人喜欢慢慢匀速滚。快速滚动时,两次事件之间可能只隔 5ms;慢速滚动时,可能隔了 80ms。

如果我们直接规定"超过 50ms 就算停了",那慢速滚动的用户每滚一下都会被切成几十个冲程,完全乱套。如果规定"超过 500ms 才算停了",那用户滚完一段稍微停顿一下再继续,程序会把两次独立操作当成一次,同样是错的。

所以,这套算法的核心思路是:不用固定标准,而是"观察你的节奏,用你自己的节奏来判断你是否停了"。

滑动窗口:给算法装上"近期记忆"

要理解用户"当前的节奏",就需要记录最近几次事件的时间间隔。代码里用的是一个大小为 5 的滑动窗口

这个"窗口"就像是算法的近期记忆 —— 它只记得最近 5 次事件之间的时间间隔,并计算它们的平均值。当新事件进来时,最老的记录会被丢掉,保持窗口大小不变。这就是滑动窗口均值的思想。

两道"关卡":判断是否该切断冲程

有了"近期平均间隔"这个参考值,算法设置了两道判断关卡,只要触发任何一道,就会把当前冲程切断,开始一个新冲程:

🚧 关卡一:绝对时间间隔(硬性截断)

规则:如果两个事件之间的间隔超过 120ms,直接切断。

120ms 对于人类感知来说大约是 1/8 秒,如果连续两次滚轮事件之间停了这么久,几乎可以肯定是用户在两次独立的操作之间停下来了。这是一道"兜底"关卡,不管你滚得多慢,停超过 120ms 必切断。

🚧 关卡二:相对时间间隔(自适应截断)

规则:如果当前间隔超过了"近期平均间隔的 2 倍"(且至少 25ms),切断。

这才是算法真正聪明的地方。判断标准随着你的滚动节奏动态调整,而不是对所有人用同一把尺子量:

用公式表达就是:

切断条件=Δt>120msORΔt>max ⁣(tˉ×2.0, 25ms)\text{切断条件} = \Delta t > 120\text{ms} \quad \mathbf{OR} \quad \Delta t > \max\!\left(\bar{t} \times 2.0,\ 25\text{ms}\right)

其中 Δt\Delta t 是当前时间间隔,tˉ\bar{t} 是近期 5 次间隔的滑动均值。

完整的检测流程

这个算法的优缺点

✅ 优点

自适应性是最大的亮点。 不同用户、不同设备、不同滚动速度,算法都能"因地制宜"地调整判断标准。触控板和机械鼠标产生的事件节奏可能差好几倍,但这套算法都能应对。

双重保险设计很稳健。 绝对阈值和相对阈值互相兜底:相对阈值应对正常场景,绝对阈值应对极端慢速或异常场景。

时间复杂度是 O(n)O(n),非常高效。 无论数据有多长,每个事件只处理一次,窗口更新是常数时间操作,完全适合实时处理。

❌ 缺点

冷启动问题。 每次切断冲程后,滑动窗口会重置。新冲程的前几个事件没有足够的历史数据,只能依赖绝对阈值(120ms)来判断。

参数是经验值,不是"真理"。 windowSize=5timeGapMultiplier=2.0absoluteTimeGap=120 都是人工调出来的,换一种输入设备或者使用场景,这些参数可能就需要重新调整。

无法区分方向变化。 这个算法只看时间,不看方向。用户向下滚然后立刻向上滚,只要时间间隔短,就会被划为同一个冲程。


算法二:抖动点识别

核心问题:哪些数据点是"噪音"?

成功把数据流切割成一段段冲程之后,下一个问题来了:每段冲程里的数据,都是可信的吗?

答案是:不一定。以下两种情况会产生"噪音数据点":

情况一:方向反转。 用户在向下滚动,但某一刻鼠标轻微抖动,产生了一个方向向上的 deltaY = -2。这个 -2 不是用户的真实意图,是物理噪音。

情况二:数值异常。 用户正常滚动,每次 deltaY 都是 3~8 之间的值,突然某次出现了 deltaY = 800。这个 800 明显不正常,可能是驱动程序 bug 或者事件合并导致的。

算法二的任务就是:在每个冲程内部,把这些异常点找出来标记。

第一阶段:方向一致性检测

核心逻辑:少数服从多数。

首先统计这个冲程里,正值(向下)和负值(向上)各有多少个,计算主方向占比:

主方向占比=max(正值个数, 负值个数)非零值总个数\text{主方向占比} = \frac{\max(\text{正值个数},\ \text{负值个数})}{\text{非零值总个数}}

如果主方向占比超过 60%,就说明这个冲程有明确的"主要方向"。此时,那些方向相反的点,就被认为是抖动噪音。

第二阶段:绝对值异常检测

对那些没有被第一阶段标记的点,做第二轮检查:如果 |deltaY| 超过 500,标记为抖动。

两阶段的完整流程

质量评分:给每个冲程打一个"健康分"

每识别出一个抖动点,就给这个冲程的质量分扣 20 分,最低 0 分:

质量分=max ⁣(0, 100抖动点数量×20)\text{质量分} = \max\!\left(0,\ 100 - \text{抖动点数量} \times 20\right)

这个算法的优缺点

✅ 优点

两种互补的检测维度,覆盖面广。 方向检测能捕捉"小值但方向错误"的抖动(如 deltaY = -1);绝对值检测能捕捉"方向正确但数值离谱"的异常(如 deltaY = 900)。两种结合,覆盖面更广。

主方向是自动推断的,不需要预设。 算法自己从数据里统计出"这个冲程主要是向哪个方向的",不需要任何预置知识。

原因分类清晰,便于诊断。 每个抖动点都记录了是因为方向问题(direction_reverse)还是数值问题(absolute_threshold),方便后续分析和调试。

❌ 缺点

60% 阈值的边界情况很微妙。 假设一个冲程只有 5 个非零值:3 正 2 负,主方向占比 = 60%,刚好在临界值上,2 个负值会被判为抖动。但如果是 2 正 2 负,占比 = 50% < 60%,则完全不做方向检测。差一个数据点,结论截然不同。

对"方向本就复杂"的场景会误报。 如果用户的操作本来就是"先向下一点,再向上一点",那少数方向的点会被错误地标记为抖动。

绝对阈值 500 过于粗糙,设备依赖性强。 触控板的 deltaY 通常是个位数,某些高精度设备可能正常值就超过 500。这个阈值对不同设备的适应性很差。

质量评分的线性扣分不够合理。 没有考虑冲程本身的长度,100个点里1个抖动和3个点里1个抖动,被同等对待,不够公平。


两种算法的横向对比

维度算法一:冲程边界检测算法二:抖动点识别
解决问题数据流切割质量过滤
核心思路时间间隔异常 → 切断方向/数值异常 → 标记
判断标准动态(历史节奏)+ 静态(120ms兜底)动态(主方向自推断)+ 静态(500兜底)
自适应性⭐⭐⭐⭐ 较强⭐⭐⭐ 中等
主要风险冷启动精度下降;参数经验依赖方向复杂时误报;设备兼容性差
可解释性⭐⭐⭐⭐⭐ 每次切断有原因⭐⭐⭐⭐⭐ 每个抖动点有原因
时间复杂度O(n)O(n)O(n)O(n)
执行顺序第一步第二步(在第一步结果上执行)