【问题标题】:How to use the AbortController to cancel Promises in React?如何使用 AbortController 取消 React 中的 Promise?
【发布时间】:2022-01-08 05:45:34
【问题描述】:

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

我的设置如下所示:

WrapperComponent.tsx: 我在这里创建 AbortController 并将信号传递给我的方法 calculateSomeStuff,该方法返回一个 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()}
    />
  );
}

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

【问题讨论】:

  • doSomeStuff 在哪里调用?您确定在设置中止信号之前没有调用它吗?
  • 该承诺将立即解决。一旦解决了,拒绝它不会有任何作用。这里的异步任务是什么?
  • @sma doSomeStuff 方法在我的 useEffect 挂钩中被调用。我不得不在这里简化代码:(
  • @parktomatomi 在 for 循环内 --> await hashChunk(chunk, hasher);叫做。计算出的值将被添加到定义在 for 循环下方的散列常量中。对不起,我不得不简化很多代码:(
  • 我之所以问,是因为“hashChunk”听起来像是一种 CPU 密集型方法,您可以在其中循环一些数据并计算哈希值。如果你不在那个 Promise 构造函数中做你的工作,或者你根本不使用awaitWorker 或其他方式释放线程,它实际上不会是异步的。它只会完成工作并返回一个已经解决的承诺。

标签: javascript reactjs async-await promise cancellation


【解决方案1】:

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

不要在 async 函数中返回 new Promise()

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

  • 设置超时
  • Web Worker 消息
  • FileReader 事件

但是在异步函数中,你的返回值已经被转换为一个 Promise。拒绝将自动转换为您可以使用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语句或throw才会得到解决。

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

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

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

当使用new Promise(...)时,必须在工作完成之前返回promise

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

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 的一切都是协同程序,没有先发制人的多任务处理,您可以在其中中止线程或强制拒绝。相反,您可以创建一个简单的令牌对象并将其传递给您长时间运行的异步函数:

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

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

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

长时间运行的工作通常涉及某种循环。只需检查循环中的令牌,如果它被取消则退出循环

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 都会返回 Promise。但实际上,它只是 WASM 加载程序上的 await-ing。在第一次运行后被缓存,之后所有的计算都是同步的。

实际上没有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 构造函数,但会立即等待而不是返回它)

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

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


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

【讨论】:

  • 不错的小说,内容不错! :P
  • 哇!谢谢你这么详细的回答。我删除了 AbortController。有些帖子不仅使用带有 fetch 的控制器,而且在我的情况下它只是不起作用。然后我按照您的描述创建了自己的令牌 - 现在它可以工作了:)。谢谢!
【解决方案2】:

我可以建议我的库 (use-async-effect2) 来管理异步任务/承诺的取消。 这是一个带有嵌套异步函数取消的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>
      );
    }

【讨论】:

    猜你喜欢
    • 2023-02-20
    • 2022-12-13
    • 2022-09-23
    • 2018-12-24
    • 2021-07-06
    • 1970-01-01
    • 2017-09-21
    • 2023-03-14
    相关资源
    最近更新 更多