Skip to content
文档章节

前端控制重复请求

背景

假设页面上有一个按钮,连续3次点击该按钮,向后端发起3次请求,记作 req1、req2、和 req3, 返回结果记作 res1、res2、和 res3。请求返回可能按照请求顺序返回,也可能不按照请求顺序返回。如果 res1, 和 res2 处在 pending 状态時,res3已经返回,则此时我们需要res3数据,res1和res2再返回,则是脏数据。如果我们需要的返回值是最新的一次请求返回值。如何处理呢。

请求队列

设计一个请求队列,只有上一次的请求返回,才执行下一个请求任务。

该方案可以保证请求结果的正确性,但是是同步处理,效率慢了些。

节流 throttle

前端按照一定的频率请求后端接口。节流是在指定时间内只执行一次操作。

节流适用在 resize, scroll, mousedown 等操作。

节流会有漏掉请求的操作。

参考 节流

防抖 debounce

防抖是指定时间内只执行最后一次任务。同样也会有漏掉请求的可能。

防抖适用于如前端搜索请求操作。

缓存请求token

将请求 key 缓存,每次请求更新最新的 token,某个请求返回验证 token是否相同,则数据是最新处理结果。

js
const cache = {};

function isValidateAsync(key) {
	const ts = Date.now();
	cache[key] = ts;

	return () => cache[key] === ts;
}

function fn(params = {}) {
	const key = JSON.stringify(params);
	const isValidateFn = isValidateAsync(key);

	asyncFn(params).then((res) => {
		if (isValidateFn()) {
			// 正确处理
		}
	});
}

XMLHttpRequest.abort()

xhr 请求可以中止

js
const xhr = new XMLHttpRequest()
xhr.open('http://127.0.0.1:4000/asyncreq')
xhr.send()

// 取消请求
xhr.abort()

Axios 使用 AbortController

前端代码

js
const axios = require('axios');

function getReqKey(config = {}) {
	const { method, url, params, data } = config;
	const reqKeyObj = {
		method,
		url,
		params,
		data,
	};
	return JSON.stringify(reqKeyObj);
}

const abortControllerMap = new Map();

function addPendingAbortController(config = {}) {
	const reqKey = getReqKey(config);

	let controller = abortControllerMap.get(reqKey);

	if (!controller) {
		controller = new AbortController();
		config.signal = controller.signal;
		abortControllerMap.set(reqKey, controller);
	}

	return controller;
}

function removePendingController(config = {}, isSelf = false) {
	const reqKey = getReqKey(config);
	const controller = abortControllerMap.get(reqKey);
	if (!isSelf && controller) {
		controller.abort();
	}

	abortControllerMap.delete(reqKey);
}

// 添加请求拦截器
axios.interceptors.request.use(
	function (config) {
		// 在发送请求之前做些什么
		removePendingController(config);
		addPendingAbortController(config);
		return config;
	},
	function (error) {
		// 对请求错误做些什么
		return Promise.reject(error);
	}
);

// 添加响应拦截器
axios.interceptors.response.use(
	function (response) {
		// 2xx 范围内的状态码都会触发该函数。

		// 对响应数据做点什么
		removePendingController(response.config, true);
		return response;
	},
	function (error) {
		removePendingController(error.config || {}, true);
		// 超出 2xx 范围的状态码都会触发该函数。
		// 对响应错误做点什么
		return Promise.reject(error);
	}
);

function onRequest(index = 1, waitTime = 0) {
	axios
		.get(`http://localhost:4000/asyncreq?waitTime=${waitTime}`)
		.then((res) => {
			console.log(`${index}次请求结果`, res.data);
		})
		.catch((error) => {
			if (axios.isCancel(error)) {
				console.log(`${index}次请求取消`, error.code);
			} else {
				console.log(`${index}次请求出错`, error.code);
			}
		});
}

onRequest(1, 4000);
onRequest(2, 4000);
onRequest(3, 4000);

后端代码

js
router.get('/asyncreq', async ctx => {
  const waitTime = Math.floor(Math.random() * 3000) + 1000;

  await delayTime(waitTime);

  ctx.body = ctx.request.query;
});

测试结果

shell
第1次请求取消 ERR_CANCELED
第2次请求取消 ERR_CANCELED
第3次请求结果 { waitTime: '4000' }

参考

  1. 多次请求同一数据接口造成数据混乱问题解决办法
  2. Axios 如何取消重复请求?| 掘金
  3. AbortController | MDN
  4. Axios