请求相关
取消请求相关
- AbortController
signal属性signal是AbortController提供的一个对象,用于传递取消信号- 实现对异步任务的取消控制
- abort方法
- 触发
abort方法会将signal.aborted设置为true - 同时会触发
signal上的abort事件。
- 触发
请求竞态
- 请求竞态在项目中,比较常见的一个问题,在异步请求或者并发请求的时候,需要考虑的问题
- 请求竞态:比如多个请求去获取数据的时候,由于网络问题或者其他的不确定原因,可能会导致先发出去的请求不一定先响应,导致数据的不一致性,资源争夺问题
场景:
- 下拉搜索:发起了多次请求,而上一次请求还没响应,而下个请求就已经响应完了,然后上一个请求响应之后,会覆盖下个请求,导致下个请求的数据被覆盖, 因此需要取消上一个请求未完成
封装请求方法hooks
- 集成try/catch
- 集成AbortController,实现取消请求
usePromise
import { get } from 'lodash-es';
type PromiseOption = {
successFn?: (result: any) => void;
errorFn?: (error: any) => void;
finallyFn?: () => void;
isAbort?: boolean; // 取消上一次请求
key?: string; // 请求的唯一key
};
// 设置signal, 必须设置key (请求key标识)
type PromiseType = Promise<any> | (({ signal }: { signal: AbortSignal }) => Promise<any>);
let globalAbortController: Record<string, AbortController | null> = {}; // 全局管理请求的上一个 AbortController
export async function usePromise(promise: PromiseType, option?: PromiseOption) {
const genKey = () => {
if (option?.key) {
return `${option.key}`;
}
return location.href;
};
const abortReq = () => {
const abort = get(globalAbortController, [genKey(), 'abort']);
if (abort) {
globalAbortController[genKey()]?.abort();
Reflect.deleteProperty(globalAbortController, genKey());
}
};
// onBeforeUnmount(() => {
// abortReq();
// });
if (option?.isAbort && option?.key && Reflect.has(globalAbortController, genKey())) {
abortReq();
}
if (option?.isAbort && option.key) {
const currentAbortController = new AbortController();
globalAbortController[genKey()] = currentAbortController;
}
try {
let result: any = null;
if (typeof promise === 'function') {
result = await promise({ signal: get(globalAbortController, [genKey(), 'signal']) });
} else {
result = await promise;
}
option?.successFn?.(result);
return [null, result];
} catch (error) {
console.log('error', error);
option?.errorFn?.(error);
return [error, null];
} finally {
option?.finallyFn?.();
const curKey = genKey();
if (option?.key && globalAbortController[curKey]) {
Reflect.deleteProperty(globalAbortController, curKey);
}
}
}
完善优化版本 usePromise
可能出现的问题
- 快速频繁请求时上一个请求未被取消
- 可能导致AbortController请求冲突, 同一个key下, 多个请求下AbortController影响
- 只想取消某一个具体请求,但由于
requestKey共享了AbortController,取消会波及到所有同requestKey的请求
解决:
- 使用
Map是为了在同一个requestKey下精确管理多个请求 - 给每个请求,都创建新的一个AbortController防止冲突
- 同
key下管理多个请求, 支持批量取消 - 使用requestId可以避免请求信息冲突,区分每个请求, 精准管理取消操作
- 通过
requestId来避免冲突, 每个请求拥有独立的AbortController,可以根据requestId精确取消某个特定的请求,而不会影响其他请求
type PromiseOption = {
successFn?: (result: any) => void;
errorFn?: (error: any) => void;
finallyFn?: () => void;
isAbort?: boolean; // 是否取消上一次请求
key?: string; // 请求的唯一 key(逻辑区分)
};
type PromiseType = Promise<any> | (({ signal }: { signal: AbortSignal }) => Promise<any>);
/**
* 一个请求key可以管理多个请求, 支持批量取消
* 给每个请求,都创建新的一个AbortController,防止冲突
*/
let globalAbortController: Record<string, Map<string, AbortController>> = {}; // 每个逻辑 key 下可以有多个请求
export async function usePromise(promise: PromiseType, option?: PromiseOption) {
console.log('globalAbortController', globalAbortController);
const genKey = () => option?.key || location.href; // 生成逻辑 key
const requestKey = genKey();
if (!globalAbortController[requestKey]) {
globalAbortController[requestKey] = new Map();
}
// 生成唯一请求 ID
const requestId = `${Date.now()}-${Math.random()}`;
const abortReq = () => {
const requestMap = globalAbortController[requestKey];
if (requestMap) {
// 取消当前逻辑 key 下的所有请求
for (const [id, controller] of requestMap.entries()) {
controller?.abort();
requestMap.delete(id);
}
}
};
// 取消请求
if (option?.isAbort) {
abortReq();
}
const newController = new AbortController();
globalAbortController[requestKey].set(requestId, newController);
try {
let result: any = null;
if (typeof promise === 'function') {
result = await promise({ signal: newController.signal });
} else {
result = await promise;
}
option?.successFn?.(result);
return [null, result];
} catch (error) {
console.log('error', error);
option?.errorFn?.(error);
return [error, null];
} finally {
globalAbortController[requestKey].delete(requestId);
// 如果当前逻辑 key 下没有未完成请求,移除该逻辑 key
if (globalAbortController[requestKey].size === 0) {
Reflect.deleteProperty(globalAbortController, requestKey);
}
option?.finallyFn?.();
}
}