题干与适用场景
一个搜索框在每次输入变化后请求建议列表。用户快速输入 hello,浏览器先后发出 h、he、hel、hell、hello 五个请求,但返回顺序不受输入顺序约束。旧请求可能最后完成,把 hello 的结果改写成 hell;用户也可能清空输入、切换网络或离开页面。
请设计请求生命周期、结果提交规则、取消策略、缓存与过期策略、加载和错误状态、无障碍反馈以及验证方案。回答要说明“界面显示的结果对应哪个查询”,不能只说给请求加防抖。
本题适合高级前端、React 和 UI 基础设施面试。React 官方文档直接用快速输入说明数据获取的 race condition,并建议在 Effect 清理时忽略过期响应;MDN 说明 AbortController.abort() 可以终止 fetch、响应体读取或流。web.dev 的 stale-while-revalidate 资料补充了以旧数据换取即时反馈的缓存取舍。这些来源支持主题的技术代表性,但不能证明任何公司的固定原题或具体频率。本题归入 frontend,因为核心能力是浏览器异步状态、渲染一致性与交互反馈。
面试官考察点
第一,看候选人能否区分请求完成和请求仍有资格提交结果。Promise 先完成不代表它对应当前查询;每次查询需要稳定身份,提交前要检查身份或查询键仍然匹配。
第二,看是否知道取消只是资源管理。AbortController 可以减少无用工作,但取消可能发生在服务端已经处理之后,不能把 abort 当作业务回滚,也不能取代过期结果保护。
第三,看能否设计完整状态机:空输入、首次加载、已有结果刷新、成功、空结果、可重试错误、过期缓存和组件卸载都应有明确规则。加载新查询时保留旧结果还是显示骨架屏,必须结合查询语义和误导风险决定。
最后,看验证是否能控制请求返回顺序,并覆盖快速输入、清空、重试、缓存命中、卸载和无障碍播报,而不是只测试网络按顺序返回的成功路径。
回答前需要澄清的问题
- 每次输入都必须请求吗? 如果只在停止输入后请求,可以使用防抖;但防抖只能减少请求数量,不能解决已经发出的请求乱序。
- 旧结果可以继续显示吗? 搜索建议通常可以在刷新期间保留旧列表,但必须标记它对应的旧查询;金融报价或权限结果可能应清空以避免误导。
- 缓存按什么键失效? 查询字符串、筛选器、语言、账户权限和数据版本都可能属于键。只按页面 URL 缓存会混入不同权限或筛选条件的数据。
- 服务端是否支持取消或请求去重? 客户端仍需要正确性保护;服务器支持取消只改善资源回收,不能保证旧请求没有副作用。
- 输入是否需要实时读屏反馈? 结果数量或加载状态可以通过已有的
status区域礼貌播报,但不能每个字符都打断用户。
30 秒回答框架
“我会把每次查询表示为带序号的请求键,只有仍等于当前查询键的响应才允许更新结果。组件清理时调用 AbortController.abort() 释放网络和读取资源,但不把取消当作回滚;即使取消失败,过期响应也会被身份检查丢弃。输入停止后再发请求只是降噪,不能代替竞态保护。状态区分空值、加载、旧结果刷新、成功、空结果和可重试错误;缓存按完整查询键存储,并设置明确的新鲜度。测试会强制让旧请求晚于新请求返回,覆盖清空、卸载、重试、缓存和读屏反馈。”
分步骤深入解答
1. 先定义结果提交不变量
维护 currentKey、status、visibleData 和可选缓存。currentKey 至少包含规范化查询字符串以及会改变结果的筛选器、语言和账户范围。每次发起请求生成唯一 requestId,闭包保存自己的 key 和 controller。
结果提交前检查两个条件:请求没有被标记为过期,且它的 key 仍等于当前 key。只有通过检查,才写入可见结果、错误或成功状态。这个不变量比“最后发起的请求通常最后返回”可靠,因为网络并不提供这种顺序保证。
可以把显示状态看作纯投影:当前查询键、最新可用缓存、当前仍有效请求和错误信息共同决定界面。不要用多个 Effect 互相同步 loading、data 和 error,否则清空输入或快速切换时容易把旧错误写回新查询。
2. 防抖、节流与竞态保护分工
防抖把连续输入合并成一次请求,适合搜索建议;节流限制固定时间窗口内的请求,适合滚动或实时监控。两者都只影响“何时发请求”,不能阻止已经发出的旧请求晚到。
即使使用 250 毫秒防抖,用户仍可能在一次请求飞行期间继续输入。请求身份检查必须保留。React 官方示例使用 Effect cleanup 设置 ignore,让旧响应不再调用 setResults;同一原则也可以用递增序号、请求键比较或状态机实现。
3. 取消是优化,不是正确性证明
每个活动请求拥有自己的 AbortController。查询键改变、输入清空、组件卸载或开始替代请求时,可以调用 abort()。捕获异常时将 AbortError 视为预期的取消结果,不把它显示成失败,也不覆盖新查询的状态。
取消信号可能来不及阻止服务器处理,也可能只终止浏览器读取。于是“取消旧请求”与“旧请求没有产生业务副作用”是两件事。搜索 GET 通常没有写副作用,但仍必须保留 requestId 检查;如果请求包含不可逆动作,取消语义更需要服务端幂等契约。
4. 选择旧结果、骨架屏和缓存
首次查询没有数据时显示加载占位和可访问的状态文本。已有结果刷新时可继续显示旧结果,同时标注它对应的查询键并显示轻量刷新指示;如果旧结果会让用户误选,则清空或降低其可操作性。
缓存键应包含完整查询输入。缓存命中可以立即展示,再在后台验证;但要记录时间、来源和错误,避免把过期或权限变化的数据当成最新事实。stale-while-revalidate 的核心是先用可接受的旧值响应,再异步获取新值,产品必须先定义可接受的新鲜度窗口。
5. 处理错误、空结果和重试
空结果是成功状态,不应显示成网络错误。错误状态要绑定当前 key;旧查询的错误到达时只能被丢弃。可重试错误保留查询输入和退避信息,用户重试时生成新的 requestId,不复用已被判定过期的 Promise。
如果返回 401、权限范围变化或服务端提示查询条件无效,应清理或重新验证缓存,而不是无限重试。网络恢复后可重新请求当前 key,但要防止恢复事件把用户已经清空的查询重新填回界面。
6. 保持键盘与辅助技术反馈
输入焦点不应因结果刷新而移动。结果容器使用稳定的列表 key,避免旧结果卸载导致屏幕阅读器重复朗读。加载、结果数量和错误可以放在预先存在的礼貌 status 区域,且只在状态发生有意义变化时更新。
键盘用户应能在加载期间继续输入、取消或选择结果;如果结果过期,选择动作要再次确认它对应的查询键。颜色不能是唯一的加载或错误线索,错误文字要与输入或列表建立明确关联。
7. 用可控顺序测试状态机
测试传输层应能暂停每个请求并手动释放响应。至少覆盖:旧请求晚于新请求成功返回;旧请求失败晚于新请求成功;用户清空后旧响应到达;缓存命中后后台刷新失败;组件卸载后响应到达;取消抛出 AbortError;重试期间再次输入。
每个事件后断言当前 key、可见结果、状态、缓存时间和播报文本。还要验证服务端返回相同结果但不同顺序时不会重复渲染或丢失焦点。性能指标可以记录请求数、取消率、过期响应丢弃数、可见结果延迟和错误重试率,但不能用更少请求掩盖错误结果。
高质量示范回答
“我先把完整查询规范化成 key,例如文本、筛选器、语言和权限范围。每次实际请求生成 requestId 和 AbortController,并记录当前 key。响应提交前必须同时通过 requestId 未过期和 key 仍匹配两个检查;失败、空结果和成功都遵守同一规则。输入变化时取消旧 controller,捕获 AbortError 后静默结束,但我不会认为 abort 已经撤销服务器处理。
我会在停止输入 250 毫秒后发请求降低噪声。首次加载显示占位;已有结果刷新时,如果旧结果仍安全,会保留并标记为上一查询的结果,否则清空。缓存按完整 key 保存,允许先显示短时间内的旧值,再后台验证并更新;缓存超过业务允许的新鲜度就只显示加载。
状态机区分空输入、加载、旧结果刷新、成功、空结果和可重试错误。错误必须绑定 key,旧请求晚到只能被丢弃。焦点保持在输入框,结果使用稳定身份,状态消息通过预先存在的礼貌区域播报,不逐字符播报。
最后我会用可控网络强制 hell 晚于 hello 返回、清空后旧响应到达、取消失败、缓存刷新失败、卸载后响应到达和重试时再次输入,并断言最终界面只解释当前 key。”
常见错误
- 只做防抖 → 已发出的请求仍可能乱序 → 保留 requestId 或 key 检查。
- 只显示最后完成的响应 → 网络完成顺序不等于查询新旧顺序 → 只允许当前 key 的响应提交。
- 把 abort 当作回滚 → 服务器可能已处理请求 → 取消用于资源回收,正确性靠身份和版本规则。
- 所有查询共用一个缓存项 → 筛选器、语言或权限可能串数据 → 缓存键覆盖全部结果输入。
- 旧错误覆盖新查询 → 用户看到已失效的报错 → 错误状态绑定 requestId 和 key。
- 首次加载和刷新都显示空白 → 用户失去可用上下文 → 按误导风险决定保留旧结果或显示骨架。
- 每次输入都播报状态 → 屏幕阅读器被连续打断 → 只播报有意义的加载、结果和错误变化。
- 只测试顺序成功 → 真实竞态没有覆盖 → 控制响应、取消、清空、卸载和重试顺序。
追问及应对
如果结果需要分页或无限滚动,key 还够吗?
需要把筛选器、排序、游标或页码纳入请求键,并把每页数据与查询版本绑定。新查询应丢弃旧分页;同一查询加载下一页时,上一页可以继续显示,但重复游标和过期页不能覆盖已确认列表。服务端若返回 next cursor,应以服务端游标为准,不能用客户端页码推断顺序。
用户在离线状态输入,重新联网时如何处理?
搜索通常不需要持久化每个旧请求;保留当前输入和最后一次可接受缓存即可。联网后只请求当前 key,取消或忽略离线期间的过期请求。若产品要求离线建议,则缓存必须记录时间、数据范围和新鲜度提示,不能把离线结果伪装成实时结果。
可以只用 React Query 或 SWR 解决吗?
缓存库能提供去重、缓存、失效和生命周期管理,但产品仍要定义查询键、结果是否可陈旧、错误重试和交互状态。库的取消或 stale-time 默认值不能替代对权限、误导风险和无障碍反馈的判断。应验证库的请求竞态语义,再把规则写进 query key 和 UI 状态。
如果搜索请求改成 POST 并记录审计,取消还安全吗?
不能假设安全。POST 可能有服务端副作用,客户端 abort 不表示事务回滚。应让服务端提供幂等键、明确提交状态和查询接口;前端只在获得权威确认后展示成功。若只是复杂查询且没有副作用,仍可使用 POST,但要把语义和重试契约写清楚。