React 中如何使用 AbortController 取消 Promise?

How to use the AbortController to cancel Promises in React?

我想在我的 React 应用程序中使用 AbortController 取消承诺,不幸的是 abort event 无法识别,因此我无法对其做出反应。

我的设置如下所示:

WrapperComponent.tsx: 在这里我创建了 AbortController 并将信号传递给我的方法 calculateSomeStuff 即 returns 一个 Promise。 controller 我将作为道具传递给我的 Table 组件。

export const WrapperComponent = () => {
  const controller = new AbortController();
  const signal = abortController.signal;

  // This function gets called in my useEffect
  // I'm passing signal to the method calculateSomeStuff
  const doSomeStuff = (file: any): void => {
    calculateSomeStuff(signal, file)
      .then((hash) => {
        // do some stuff
      })
      .catch((error) => {
        // throw error
      });
  };

  return (<Table controller={controller} />)
}

calculateSomeStuff 方法如下所示:

export const calculateSomeStuff = async (signal, file): Promise<any> => {
  if (signal.aborted) {
    console.log('signal.aborted', signal.aborted);
    return Promise.reject(new DOMException('Aborted', 'AbortError'));
  }

  for (let i = 0; i <= 10; i++) {
    // do some stuff
  }

  const secret = 'ojefbgwovwevwrf';

  return new Promise((resolve, reject) => {
    console.log('Promise Started');
    resolve(secret);

    signal.addEventListener('abort', () => {
      console.log('Aborted');
      reject(new DOMException('Aborted', 'AbortError'));
    });
  });
};

在我的 Table 组件中,我这样调用 abort() 方法:

export const Table = ({controller}) => {
  const handleAbort = ( fileName: string) => {
    controller.abort();
  };

  return (
    <Button
      onClick={() => handleAbort()}
    />
  );
}

我在这里做错了什么?在调用 handleAbort 处理程序后,我的 console.logs 不可见并且 signal 从未设置为 true

根据您的代码,需要进行一些更正:

不要在 async 函数中 return new Promise()

如果您采用基于事件但自然异步的方式并将其包装到 Promise 中,则使用 new Promise。示例:

  • 设置超时
  • 网络工作者消息
  • FileReader 事件

但在异步函数中,您的 return 值将已经转换为承诺。拒绝将自动转换为您可以使用 try/catch 捕获的异常。示例:

async function MyAsyncFunction(): Promise<number> {
  try {
    const value1 = await functionThatReturnsPromise(); // unwraps promise 
    const value2 = await anotherPromiseReturner();     // unwraps promise
    if (problem)
      throw new Error('I throw, caller gets a promise that is eventually rejected')
    return value1 + value2; // I return a value, caller gets a promise that is eventually resolved
  } catch(e) {
    // rejected promise and other errors caught here
    console.error(e);
    throw e; // rethrow to caller
  }
}

调用者会立即得到一个 promise,但在代码遇到 return 语句或抛出之前它不会被解析。

如果您有工作需要用 Promise 构造函数包装,并且您想从 async 函数中完成怎么办?将 Promise 构造函数放在一个单独的非 async 函数中。然后 await 来自 async 函数的非 async 函数。

function wrapSomeApi() {
  return new Promise(...);
}

async function myAsyncFunction() {
  await wrapSomeApi();
}

使用 new Promise(...) 时,必须 returned 承诺才能 工作完成

您的代码应大致遵循以下模式:

function MyAsyncWrapper() {
  return new Promise((resolve, reject) => {
    const workDoer = new WorkDoer();
    workDoer.on('done', result => resolve(result));
    workDoer.on('error', error => reject(error));
    // exits right away while work completes in background
  })
}

您几乎从未想要使用Promise.resolve(value)Promise.reject(error)。这些仅适用于您的接口 需要 承诺但您已经拥有价值的情况。

AbortController 仅适用于 fetch

运行 TC39 的人们一直在努力解决取消问题 for a while,但现在还没有官方取消 API。

AbortControllerfetch 接受用于取消 HTTP 请求,这很有用。但这并不意味着取消常规的旧工作。

幸运的是,你可以自己做。 async/await 中的所有内容都是协同例程,没有抢先式多任务处理,您可以在其中中止线程或强制拒绝。相反,您可以创建一个简单的令牌对象并将其传递给您的 long 运行ning 异步函数:

const token = { cancelled: false }; 
await doLongRunningTask(params, token); 

要取消,只需更改 cancelled 的值即可。

someElement.on('click', () => token.cancelled = true); 

长 运行ning 工作通常涉及某种循环。只检查循环中的token,如果取消就退出循环

async function doLongRunningTask(params: string, token: { cancelled: boolean }) {
  for (const task of workToDo()) {
    if (token.cancelled)
      throw new Error('task got cancelled');
    await task.doStep();
  }
}

由于您使用的是 React,因此您需要 token 在渲染之间成为相同的引用。因此,您可以为此使用 useRef 挂钩:

function useCancelToken() {
  const token = useRef({ cancelled: false });
  const cancel = () => token.current.cancelled = true;
  return [token.current, cancel];
}

const [token, cancel] = useCancelToken();

// ...

return <>
  <button onClick={ () => doLongRunningTask(token) }>Start work</button>
  <button onClick={ () => cancel() }>Cancel</button>
</>;

hash-wasm 只是半异步的

您提到您正在使用 hash-wasm。这个库 看起来 是异步的,正如其所有 API 的 return 所承诺的那样。但实际上,它只是 await-ing 在 WASM 加载器上。在第一个 运行 之后被缓存,之后所有计算都是同步的。

实际上 await 没有的异步代码没有任何好处。它不会暂停以解除线程阻塞。

那么,如果你有像 hash-wasm 使用的 CPU 密集型代码,你怎么能让你的代码呼吸呢?您可以逐步完成工作,并使用 setTimeout:

安排这些增量
for (const step of stepsToDo) {
  if (token.cancelled)
    throw new Error('task got cancelled');

  // schedule the step to run ASAP, but let other events process first
  await new Promise(resolve => setTimeout(resolve, 0));

  const chunk = await loadChunk();
  updateHash(chunk);
}

(请注意,我在这里使用了 Promise 构造函数,但立即等待而不是 returning 它)

上面的技术 运行 比只做任务慢。但是通过让出线程,像 React 更新这样的东西可以在没有尴尬挂起的情况下执行。

如果您确实需要性能,请查看 Web Workers,它可以让您执行 CPU-繁重的线程外工作,因此不会阻塞主线程。 workerize 等库可以帮助您将异步函数转换为 worker 中的 运行。


暂时就这些了,写小说不好意思

我可以推荐我的库 (use-async-effect2) 来管理异步取消 tasks/promises。 这是一个带有嵌套异步函数取消的 simple demo

    import React, { useState } from "react";
    import { useAsyncCallback } from "use-async-effect2";
    import { CPromise } from "c-promise2";
    
    // just for testing
    const factorialAsync = CPromise.promisify(function* (n) {
      console.log(`factorialAsync::${n}`);
      yield CPromise.delay(500);
      return n != 1 ? n * (yield factorialAsync(n - 1)) : 1;
    });
    
    function TestComponent({ url, timeout }) {
      const [text, setText] = useState("");
    
      const myTask = useAsyncCallback(
        function* (n) {
          for (let i = 0; i <= 5; i++) {
            setText(`Working...${i}`);
            yield CPromise.delay(500);
          }
          setText(`Calculating Factorial of ${n}`);
          const factorial = yield factorialAsync(n);
          setText(`Done! Factorial=${factorial}`);
        },
        { cancelPrevious: true }
      );
    
      return (
        <div>
          <div>{text}</div>
          <button onClick={() => myTask(15)}>
            Run task
          </button>
          <button onClick={myTask.cancel}>
            Cancel task
          </button>
        </div>
      );
    }