Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC]是否考虑在useRequest中引入AbortController #2593

Open
haoqi0818 opened this issue Jul 11, 2024 · 8 comments
Open

[RFC]是否考虑在useRequest中引入AbortController #2593

haoqi0818 opened this issue Jul 11, 2024 · 8 comments
Labels
feature New feature or request v4

Comments

@haoqi0818
Copy link

haoqi0818 commented Jul 11, 2024

目前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
#1715 (comment) 目前AbortController应该不再属于实验性质了

@haoqi0818
Copy link
Author

看了一下,有很多类似执行取消或者监听取消的诉求
#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();

@crazylxr
Copy link
Collaborator

如果要解决这个问题,是不是直接在 onBefore 里支持取消 promise 的执行就好了,比如 onBefore 里如果返回的是 false,onRequest 就不执行了。

@haoqi0818
Copy link
Author

如果要解决这个问题,是不是直接在 onBefore 里支持取消 promise 的执行就好了,比如 onBefore 里如果返回的是 false,onRequest 就不执行了。

onBefore是service还没有开始执行的时候吧,我的场景是service已经开始执行,但还没结束(fetch请求已发出没收到响应)

@crazylxr
Copy link
Collaborator

合理,整,不过我看 fetch 的 signal,兼容性没那么好,到时候可能需要注意一下。

@crazylxr crazylxr added feature New feature or request v4 labels Jul 24, 2024
@crazyair
Copy link

crazyair commented Aug 2, 2024

合理,整,不过我看 fetch 的 signal,兼容性没那么好,到时候可能需要注意一下。

这个还好,那也是请求库的事,用户可以自己实现取消,https://tanstack.com/query/v4/docs/framework/react/guides/query-cancellation
image

@crazyair
Copy link

crazyair commented Aug 2, 2024

另外如果要中断的话,自己封装下 useRequest 也可以

  1. 外面声明 CancelToken
  2. useRequest 第一个参数传透传
  3. 在 onCancel 执行 cancel

@Veveue
Copy link

Veveue commented Aug 15, 2024

稍加包装一下就行

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;

@haoqi0818
Copy link
Author

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;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request v4
Projects
None yet
Development

No branches or pull requests

4 participants