[RFC]是否考虑在useRequest中引入AbortController
目前useRequest的做法,是通过标记在竞态问题出现时,标记忽略之前请求回调(onSuccess)执行。 新加的cancel方法可以主动标记,让service执行结束后的回调(onSuccess)不再执行。
const { cancel } = useRequest(async () => {
const res = await fetch(url);
return res.data;
}, { onSuccess })
但是,这些对于并不能终止service内部的执行。
使用AbortController可以实现fetch的提前中止
const abortControllerRef = useRef();
const { cancel } = useRequest(async () => {
const controller = new AbortController();
abortControllerRef.current = controller;
const res = await fetch(url, { signal: controller.signal });
return res.data;
}, { onSuccess })
const myCancel = () => {
abortControllerRef.current?.abort();
abortControllerRef.current = null;
}
自己实现过于繁琐,看看能否直接在useReuqest的时候,默认生成一个controller,传递给service方法,并且在合适时机自动调用abort方法(竞态时、cancel时)。
// 理想中的接口使用
const { cancel } = useRequest(async (controller) => {
// service中如果包含了多个异步阶段,可以很方便的在任一阶段提前中止
const res1 = await fetch(url1, { signal: controller.signal });
const res2 = await fetch(url2, { data: getDataDepsRes1(), signal: controller.signal });
const res3 = await fetch(url3, { data: getDataDepsRes2(), signal: controller.signal });
const res4 = await fetch(url4, { data: getDataDepsRes3(), signal: controller.signal });
return res.data;
}, { onSuccess })
虽然说覆水难受,已发出去的请求服务器仍然会收到,但仍然一定以上能过做到性能提升
- fetch请求的结果解析,以及一些封在api方法的后置逻辑,均能被最大程度省掉
- 对于一些长pending状态的请求也可以提前释放,结束掉闭包引用等
#862 #2039 https://github.com/alibaba/hooks/issues/1715#issuecomment-1166513696 目前AbortController应该不再属于实验性质了
看了一下,有很多类似执行取消或者监听取消的诉求 #2371 #1826 #1634 #1498 通过AbortController可以比较友好的实现,像自定义的Promise也可以通过它的事件方式实现中止
const controller = new AbortController();
const signal = controller.signal;
new Promise((resolve, reject) => {
setTimeout(resolve, 3000);
signal.addEventListener("abort", () => {
console.log("Request aborted");
reject();
});
});
controller.abort();
如果要解决这个问题,是不是直接在 onBefore 里支持取消 promise 的执行就好了,比如 onBefore 里如果返回的是 false,onRequest 就不执行了。
如果要解决这个问题,是不是直接在 onBefore 里支持取消 promise 的执行就好了,比如 onBefore 里如果返回的是 false,onRequest 就不执行了。
onBefore是service还没有开始执行的时候吧,我的场景是service已经开始执行,但还没结束(fetch请求已发出没收到响应)
合理,整,不过我看 fetch 的 signal,兼容性没那么好,到时候可能需要注意一下。
合理,整,不过我看 fetch 的 signal,兼容性没那么好,到时候可能需要注意一下。
这个还好,那也是请求库的事,用户可以自己实现取消,https://tanstack.com/query/v4/docs/framework/react/guides/query-cancellation
另外如果要中断的话,自己封装下 useRequest 也可以
- 外面声明 CancelToken
- useRequest 第一个参数传透传
- 在 onCancel 执行 cancel
稍加包装一下就行
function useRequestWithAbort(service, { manual }) {
const [responseData, setResponse] = useState({});
const abortControllerRef = useRef(null);
const { loading, run, cancel } = useRequest(service, {
manual: manual, // 是否手动触发
onSuccess: (response) => {
setResponse(response);
},
onError: (error) => {
console.log(error);
// notification.error({
// message: "请求错误",
// description: error.message,
// });
},
});
const handleRequest = async (params = {}) => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
return run({ signal: abortControllerRef.current.signal, ...params });
};
const handleCancel = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
cancel();
// console.log("请求取消");
};
return { handleRequest, handleCancel, loading, responseData };
}
export default useRequestWithAbort;
import { useRef } from 'react';
import { useRequest } from 'ahooks';
type IService<TData, TParams extends any[]> = (signal: AbortSignal, ...args: TParams) => Promise<TData>;
type IOptions<TData, TParams extends any[]> = Parameters<typeof useRequest<TData, TParams>>[1];
type IPlugins<TData, TParams extends any[]> = Parameters<typeof useRequest<TData, TParams>>[2];
/**
* 对ahooks的useRequest做二次封装,自动未每一次请求生成AbortController
*/
const useRequestWithAbortController = <TData, TParams extends any[]>(
service: IService<TData, TParams>,
options?: IOptions<TData, TParams>,
plugins?: IPlugins<TData, TParams>,
) => {
const abortControllerRef = useRef<AbortController>();
const ret = useRequest<TData, TParams>(
(...params) => {
if (abortControllerRef.current) {
abortControllerRef.current.abort('竞态终止');
}
const controller = new AbortController();
abortControllerRef.current = controller;
return service.call(this, controller.signal, ...params);
},
{
...options,
onFinally: (...args) => {
abortControllerRef.current = undefined;
options?.onFinally?.(...args);
},
},
plugins,
);
return { ...ret, abortController: abortControllerRef.current };
};
export default useRequestWithAbortController;
This might be solved? useAsyncMemo with direct support for AbortSignal: https://github.com/HK-SHAO/react-client-async