防重提交是个老生常谈的问题,使用外部变量锁定或修改按钮状态的方式方式比较繁琐冗余,
而知乎的哥们在 怎样防止重复发送 Ajax 请求?的问答上,提到了防重提交的几个方式,
根据实际项目的需求,采用了A. 独占型提交
+ D. 懒惰型提交
组合方式,代码实现如下:
// http.js import { debounce } from "../debounce"; let pendingArr = []; let CancelToken = axios.CancelToken; let pendingHandler = (flag, cancelFn) => { if (pendingArr.indexOf(flag) > -1) { if (cancelFn) { cancelFn(); // cancel } else { pendingArr.splice(pendingArr.indexOf(flag), 1); // remove flag } } else { if (cancelFn) { pendingArr.push(flag); } } }; // request interceptor axios.interceptors.request.use( config => { config.cancelToken = new CancelToken(cancelFn => { pendingHandler(config.baseURL + config.url + "&" + config.method, cancelFn); }); return config; }, err => { return Promise.reject(err); } ); // response interceptor axios.interceptors.response.use( response => { pendingHandler(response.config.url + "&" + response.config.method); return response; }, err => { pendingArr = []; return Promise.reject(err); } ); return debounce( axios(config) .then(response => { // handle response resolve(response.data) }) .catch(thrown => { if (axios.isCancel(thrown)) { console.log("Request canceled", thrown.message); } else { let { response, request, message } = thrown; reject(message); } }), 500, true );
// debounce.js export function debounce(func, wait, immediate) { var timeout, args, context, timestamp, result; if (null == wait) wait = 100; function later() { var last = Date.now() - timestamp; if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { timeout = null; if (!immediate) { result = func.apply(context, args); context = args = null; } } } var debounced = function() { context = this; args = arguments; timestamp = Date.now(); var callNow = immediate && !timeout; if (!timeout) timeout = setTimeout(later, wait); if (callNow) { result = func.apply(context, args); context = args = null; } return result; }; debounced.clear = function() { if (timeout) { clearTimeout(timeout); timeout = null; } }; debounced.flush = function() { if (timeout) { result = func.apply(context, args); context = args = null; clearTimeout(timeout); timeout = null; } }; return debounced; }
这里用到了 axios 拦截器,初始化一个pendingArr
数组,用于存放正在 pending 的请求,以url
+method
为标志,
在 http response 拦截器删除完成 pending 的请求,在 http request 的拦截器中,先判断是否存在正在 pending 的同一个请求,若无则把请求标志位存入数组,若存在,则取消请求。
到这里还未结束,前面做的是终止请求,但是请求已经发出,短时间内频繁请求,会对服务器造成一定压力,
所以我这里用了一个debounce (防抖动)
,规定时间内把触发非常频繁的事件合并成一次执行,比如我这里是500ms 内,触发很频繁的请求都会合并成一次执行,避免用户疯狂点击,触发多次请求的情况。
至于debounce
的实现,我这里是摘取underscore
源码,至于原理可以参考Debouncing and Throttling Explained Through Examples,这里阐述了 debounce (防抖动)
、throttling(节流阀)
的原理及其异同。
简化版:
let pending = []; //声明一个数组用于存储每个ajax请求的取消函数和ajax标识 let cancelToken = axios.CancelToken; let removePending = (config) => { for(let p in pending){ if(pending[p].u === config.url + '&' + config.method) { //当当前请求在数组中存在时执行函数体 pending[p].f(); //执行取消操作 pending.splice(p, 1); //把这条记录从数组中移除 } } } //添加请求拦截器 axios.interceptors.request.use(config=>{ removePending(config); //在一个ajax发送前执行一下取消操作 config.cancelToken = new cancelToken((c)=>{ // 这里的ajax标识我是用请求地址&请求方式拼接的字符串,当然你可以选择其他的一些方式 pending.push({ u: config.url + '&' + config.method, f: c }); }); return config; },error => { return Promise.reject(error); }); //添加响应拦截器 axios.interceptors.response.use(response=>{ removePending(res.config); //在一个ajax响应后再执行一下取消操作,把已经完成的请求从pending中移除 return response; },error =>{ return { data: { } }; 返回一个空对象,否则控制台报错 });