用户先搜甲再搜乙,乙先返回、甲后返回时,旧结果可能覆盖当前输入。
本文使用ECMAScript 5.1 与浏览器 DOM,代码可放入独立演示页。JavaScript(脚本语言)的作用域、函数调用与事件调度,需要与浏览器提供的接口区分。本文主体使用当时已经可用的语法,不把语言版本和宿主环境混为一谈。
1. 从现象推到机制
异步请求完成顺序不受发出顺序保证。取消旧请求可以减少浪费,但不能单独依赖取消成功。为每次查询分配递增序号,渲染时只接受最新序号对应的结果,就能明确结果归属。
图中的四个节点分别表示输入变化、异步请求、结果归属、页面更新。顺着实线检查输入如何变成输出,再沿虚线核对反馈条件。图示表达的是本文讨论的关系,不是实测曲线或现场截图。
2. 最小实现与输入条件
用 XMLHttpRequest 的演示封装发请求,保存当前序号。服务端故意设置不同延迟,在回调中比较序号与当前输入状态。
1
2
3
4
5
6
7
8
var sequence = 0;
function search(query) {
var mine = ++sequence;
request(query, function(result) {
if (mine !== sequence) return;
render(result);
});
}
先记录输入、调用位置与控制台结果,再逐个改变条件。事件和异步实验保留触发顺序,避免在生产页面执行带有未知副作用的调试代码。
3. 用对照区分原因
下表列出需要主动改变的条件。预期行为来自机制分析,复现时还需记录运行环境和原始输出,不能将预期当作已经测得的结果。
| 改变的条件 | 需要验证的行为 |
|---|---|
| 旧请求晚于新请求返回 | 旧结果不得覆盖当前列表 |
| 用户清空关键词 | 也应更新当前查询身份 |
| 尝试取消旧请求 | 仍保留归属校验更稳妥 |
对照 1:旧请求晚于新请求返回
旧结果不得覆盖当前列表。观察时记录请求序号与当前序号。把这个条件与基准情形分开,有助于判断变化来自哪一层;若得到相反结果,应先核对前提,再修改实现。
对照 2:用户清空关键词
也应更新当前查询身份。观察时记录清空后的页面状态。把这个条件与基准情形分开,有助于判断变化来自哪一层;若得到相反结果,应先核对前提,再修改实现。
对照 3:尝试取消旧请求
仍保留归属校验更稳妥。观察时记录取消与回调之间的竞争。把这个条件与基准情形分开,有助于判断变化来自哪一层;若得到相反结果,应先核对前提,再修改实现。
4. 适用条件与取舍
防抖减少请求频率,序号控制结果归属,两者解决的问题不同。分页或多区域请求还应有各自独立的查询身份。
一次正常运行只覆盖了这组输入。继续扩展前,应把上面的异常对照保留下来,尤其关注“尝试取消旧请求”这一分支。后续方案若改变了图中任意一处依赖,原来的判断就要重新验证,不能仅凭界面相似或返回值相同沿用结论。