定时与防抖工具包(dt)设计与实现

3 阅读4分钟

定时与防抖工具包(core/pkg/dt)设计与实现

1. 包做什么

dt(delay / timer)封装与时间窗口相关的常用能力,减少业务侧手写 Timer / Ticker / 竞态处理。

API作用
SetTimeout延迟执行一次,支持取消
SetInterval按固定间隔重复执行,直到取消
Debounce每次调用将执行时刻推迟到 now+interval,全局轮询到点后执行一次并 Remove key
TrailingDebounce同一 key 连续触发时取消上次未到期任务,仅在「最后一次触发」后再静默 duration 执行(尾部防抖)
Throttle同一 key 在滑动 duration 窗口内仅首次调用立即执行,窗口内其余丢弃(前缘节流)
ThrottleFixedGridTrailing从首次调用建立 epoch,按固定 period 对齐分槽;每槽右边界执行该槽内最后一次 call;空闲时 Remove key,并带周期清理

术语:口语里「节流 / 防抖」常混用。本包中 首触限频Throttle末触合并TrailingDebounceDebounce对齐时间轴、按槽尾执行ThrottleFixedGridTrailing


2. 各函数语义与实现要点

2.1 SetTimeout

  • 到期执行 f 一次;返回 cancel,提前调用则停止 Timer 并在必要时 drain,减轻计时器残留。

2.2 SetInterval

  • Ticker 循环执行 f 直至 cancel首帧在第一个 interval 之后(与常见 setInterval 一致,非立即首帧)。

2.3 Debounce(uniqueId, interval, call)

  • debounceMapsExecTime = now + intervaldebounceRunner(约 10ms 步进)扫描,到点 go call()Remove(uniqueId)
  • 空槽不执行;每次调用都会重置截止时间。

2.4 TrailingDebounce(uniqueId, duration, call)

  • trailingDebounceMaps*throttledTypeCancel + Call);每次调用取消旧 SetTimeout,再排新的 duration
  • 定时器回调用 entry 指针与 map 内现条目比对,避免被替换后旧定时器误执行新回调。

2.5 Throttle(uniqueId, duration, call)

  • 每 key throttleEntrysync.Mutex + lastExecnow.Sub(lastExec) < duration 则丢弃。
  • TrailingDebounce 对照:节流保首次,尾部防抖保末次

2.6 ThrottleFixedGridTrailing(uniqueId, period, call)

  • epoch:该 key 首次成功入队时的 now,之后槽为 [epoch+k·period, epoch+(k+1)·period)
  • 同槽多次调用只更新 pendingCall;在槽右边界 epoch+(k+1)·periodAfterFunc 触发 onSlotTimerFire(锁内 flush,锁外 go call())。
  • 跨槽:若新调用槽号大于当前 pendingSlot 且仍有未执行 pending,先 Stop 定时器同步补跑上一槽最后一次,再为当前槽重排期。
  • map 清理tryRemoveFixedGridEntryIfIdlependingCall==niltimer==nil、且 map 中仍指向本 entry 时 RemoveonSlotTimerFire 末尾会调用;另有 10s 一次的 throttleFixedGridIdleRunner 兜底。
  • 并发ThrottleFixedGridTrailing 在持 entry.mu 后再次 Get 校验 v==entry,不匹配则解锁重试(避免已删键仍操作entry)。
  • 键被移除后,同一 uniqueId 再次调用会重新建立 epoch(新时间轴)。

通俗的说10.2s内我执行了100次,我传入了500毫秒为一个执行周期,实际上只会触发执行21次,等于说无论我调用多少次, 只会在开始时间开始计时500毫秒为一个周期执行这个周期内的最后一次调用,尾部不足500ms的内容会在10.5s执行,总共执行了21次。


3. 时序示意

3.1 TrailingDebounce(尾部防抖)

sequenceDiagram
	participant U as 调用方
	participant M as trailingDebounceMaps
	participant T as Timer
	U->>M: TrailingDebounce(k,d,f1)
	M->>T: 启动 d 后回调
	U->>M: TrailingDebounce(k,d,f2)
	M->>T: 取消上一 Timer,重新 d
	T->>M: 到期且 entry 仍为当前指针
	M->>U: 仅执行 f2

3.2 Throttle(前缘节流)

sequenceDiagram
	participant U as 调用方
	participant M as throttleMaps
	U->>M: Throttle(k,d,f)
	M->>U: go f()(窗口首次)
	U->>M: Throttle(k,d,f)(窗内)
	M->>U: 丢弃

3.3 Debounce(防抖)

sequenceDiagram
	participant U as 调用方
	participant M as debounceMaps
	participant R as debounceRunner
	U->>M: Debounce(k,i,f) 推迟 ExecTime
	R->>M: tick >= ExecTime
	R->>U: go f(),Remove(k)

3.4 ThrottleFixedGridTrailing(固定栅格尾部节流)

sequenceDiagram
	participant U as 调用方
	participant M as throttleFixedGridMaps
	participant T as AfterFunc
	U->>M: 本槽内多次更新 pendingCall
	T->>M: 槽右边界 onSlotTimerFire
	M->>U: go call(),空闲则 Remove(k)

4. 并发与依赖

  • Map:github.com/orcaman/concurrent-map
  • Throttlesync.Mutex 每 entry。
  • TrailingDebounce / ThrottleFixedGridTrailing:entry 级互斥 + 指针或 map 二次校验。
  • 回调多为 go call() 异步执行,业务回调内需自行保证并发安全。

5. 使用建议

SetTimeoutSetInterval 与javascript类似

场景推荐 API
延迟执行SetTimeout
固定周期执行SetInterval
停止调用后后执行最后一次调用TrailingDebounce
停止调用后后执行第一次调用Throttle
周期性执行周期内最后一次调用ThrottleFixedGridTrailing
依赖全局10ms粒度执行Debounce

同一业务 key 避免混用多套语义不同的 API。


6. 测试说明(core/pkg/dt/dt_test.go

测试函数覆盖点
TestSetTimeout_CancelSkipsCallback取消后无回调
TestSetInterval_FiresMultipleTimesBeforeCancel周期触发至少 2 次
TestDebounce_ResetsDeadlineOnRepeatCall推迟截止、最终 1 次
TestTrailingDebounce_MergesToSingleExecution末触合并
TestTrailingDebounce_ReplacedScheduleDoesNotFireStaleentry 指针竞态
TestThrottle_LeadingEdgeOncePerWindow前缘节流 + 窗后可再触发
TestThrottleFixedGridTrailing_SlotCountMatchesTimeline约 102ms/50ms → 3 次
TestThrottleFixedGridTrailing_RemovesIdleKeyFromMap执行后 map 无 key
TestThrottleFixedGridTrailing_LastCallWinsInSlot同槽闭包覆盖

运行:

go test ./core/pkg/dt/... -race

7. 小结

dt 将定时、防抖、节流与固定栅格槽尾执行统一到少数 API: TrailingDebounceThrottle 解决多数末触 / 首触问题;ThrottleFixedGridTrailing 适合整段对齐周期、且需控制 map 生长的长连接/报表类场景; Debounce 适合能接受全局轮询步长的轻量推迟执行。