项目中有时遇到一些连续用不同参数调用同一个接口的情况,当服务端有问题的时候可能会出现前一个请求返回的数据把后一个的数据覆盖掉的问题。比如:
点击A公司,获取其员工列表,但数据迟迟没有返回,等不耐烦了,点击B公司,B公司的数据很快返回并被渲染到列表中。再过了一会,A公司的数据返回,列表中的数据被替换,而此时选择的公司为B,出现不一致。
此外比如typeahead、自动搜索等也可能出现类似的情况。
第一次遇到这种问题时,想到的办法是在调接口前给DOM元素加一个属性,值为传给接口的参数,比如公司ID,请求成功返回数据后,读取DOM上的属性,和当前传给接口的参数比较,两者相同才渲染。拿上面的例子来说,代码大概像这样:
function listEmployees(companyid) {
$('#employee_list').data('companyid', companyid);
var params = {
api: 'business',
action: 'list_employees',
companyid: companyid
}
var ok = function(r) {
var domCompanyid = $('#employee_list').data('companyid');
if (domCompanyid == companyid) {
renderList(r);
}
}
return http.get(params).then(ok);
}
笨办法,起作用,但是一来麻烦,得手动给所有可能会遇到这种问题的函数加判断;二来前面的请求其实已经没用了,为什么还要等它返回再判断一下,完全可以取消掉。
项目是用封装过的axios来处理请求的,而axios支持取消请求,所以直接拿来用。至于背后的逻辑,我还没搞懂,有兴趣的朋友可以参考这里。
直接上代码吧。
// 全局对象用于存储cancel函数
var cancelCandidates = {};
// 请求拦截
axios.interceptors.request.use(function (config) {
// ...
var params = JSON.parse(config.params);
var cancelID = params.api+'/'+params.action; // 作为重复请求的标识
var cancelFn = cancelCandidates[cancelID]; // 根据标识尝试获取已有的cancel函数
if (cancelFn && typeof cancelFn === 'function') {
cancelFn(); // 如果已有,说明当前请求与前一请求重复,且前一请求尚未完成,取消前一请求
}
var source = axios.CancelToken.source(); // 针对当前请求生成token和cancel函数
config.cancelToken = source.token; // 将token添加到请求参数
cancelCandidates[cancelID] = source.cancel; // 将cancel函数存入全局对象
return config;
}, function (error) {
// ...
});
// 响应拦截
axios.interceptors.response.use(function (response) {
var params = JSON.parse(response.config.params);
var cancelID = params.api+'/'+params.action;
delete cancelCandidates[cancelID]; // 请求完成后删除对应的cancel函数
return response;
}, function (error) {
if (axios.isCancel(error)) { // 被取消的请求会当作错误处理
console.warn('request cancelled')
return Promise.resolve('cancelled')
} else {
// ...
}
});
最终效果如下,项目中的自动搜索功能,输入关键字一段时间后才会触发搜索,按下退格会立刻触发搜索。