目标
经典 typeahead bug 是防抖了错误的对象:输入值被延迟,按键像被吃掉,请求照样风暴。正确做法是只对昂贵的副作用(fetch)防抖,输入值同步更新,并取消一切被取代的东西——计时器与在途请求双双清场,再加一道请求 id 保险带,打字与网络同时安静下来。
步骤要点
- 状态分两个:query 每次按键同步更新保证输入即时回显,results 只在防抖副作用里写入。
- effect 里 setTimeout 300ms 后发请求,cleanup 同时 clearTimeout 与 AbortController.abort,新按键到来时未发的与在途的一起取消。
- 加单调递增请求 id 守卫:响应回来时若不是最新 id 就丢弃,即使某些环境不支持 abort 也不会被旧结果覆盖。
- 语义定为尾随触发:搜索在停顿之后发出而非首键即发,清空输入则完全绕过防抖立即复位。
- 用 Playwright 脚本验证爆发输入:80ms 一键连打 “checkout” 应恰好产生一个请求,两段式输入恰好两个请求。
排错提示
输入本身发卡是把防抖包在了状态更新上而不是副作用上,把受控值改成同步更新、只让 fetch 等待;响应仍互相覆盖说明取消不完整,cleanup 里要同时清计时器和 abort 请求,再留 id 守卫给不支持 AbortController 的传输;服务端连防抖后的量都扛不住就把延迟提到 350-500ms、设最小两到三字符、并对相同查询做缓存让重复输入零成本。
适用边界
防抖是拿“逐键即时搜索”换平静,产品确实要逐字即搜就得靠服务端限流;300ms 是默认值不是法律,量测你的用户打字节奏再调,且任何延迟下取消路径都必须严丝合缝。
代码示例
before (debounce on state):
keystrokes: c-h-e-c-k-o-u-t
input echo: lags 300ms behind, chars "lost" visually
requests: 8 (one per char), out-of-order overwrites
after (debounce on effect + abort):
keystrokes: c-h-e-c-k-o-u-t (80ms apart)
input echo: instant, every char visible
requests: 1, q=checkout, fired 300ms after last key
aborts: 7 timers cleared, 0 in-flight (none sent)
burst test (Playwright):
fill("checkout") -> expect requests == ["checkout"]