如何在React中使用AbortController取消Promise?

问题描述 投票:0回答:3

我想使用

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

我的设置如下:

WrapperComponent.tsx: 在这里,我创建 AbortController 并将信号传递给返回 Promise 的方法

calculateSomeStuff
。我将
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 不可见,并且在调用

signal
处理程序后,
true
永远不会设置为
handleAbort

javascript reactjs async-await promise cancellation
3个回答
13
投票

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

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

如果您要处理基于事件但自然异步的内容,则可以使用

new Promise
,并将其包装到 Promise 中。例子:

  • 设置超时
  • Web Worker 消息
  • 文件读取器事件

但是在异步函数中,您的返回值将已经转换为承诺。拒绝将自动转换为您可以使用

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(...)
时,必须在工作完成之前返回承诺

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

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 的人们一直在尝试解决取消问题有一段时间了,但目前还没有官方的取消 API。

AbortController
fetch
接受以取消 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。第一次运行后会被缓存,之后所有计算都是同步的。

即使它被包装在

async
函数或返回
Promise
的函数中,代码也必须产生线程以并发操作,而 hash-wasm 似乎在其主计算循环中没有这样做。

那么,如果您拥有像 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 这样的库可以帮助您将异步函数转换为在工作线程中运行。


这就是我现在所拥有的一切,我很抱歉写小说


0
投票
我可以建议我的库(

use-async-effect2)来管理异步任务/承诺的取消。 这是一个带有嵌套异步函数取消的简单演示

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> ); }
    

0
投票
使用自定义挂钩“useFetchWithCancellation”来处理此问题。

useFetchWithCancellation.js

import { useState } from 'react'; import { useCallback, useEffect } from 'react'; function fetchWithCancellation(url, options) { return new Promise(async (resolve, reject) => { try { const response = await fetch(url, options); resolve(response); } catch (error) { if (error.name === 'AbortError') { //reject(error); } else { reject(error); } } }); } function useFetchWithCancellation(from) { const [controller, setController] = useState(new AbortController()); useEffect(() => { setController(new AbortController()); return () => { return controller.abort(); }; }, []); const fetchData = useCallback((url, options) => { let opts = {}; if (options) opts = options; return fetchWithCancellation(url, { ...opts, signal: controller.signal }); }, []); return { fetchData } } export default useFetchWithCancellation;

MyComponent.js

import useFetchWithCancellation from './useFetchWithCancellation'; const MyComponent = () => { const { fetchData } = useFetchWithCancellation(); callAPI = ()=>{ fetchData("api", { method: "POST", body: formData }).then(response => response.json()).then((res) => { //do something }); } export default MyComponent
    
© www.soinside.com 2019 - 2024. All rights reserved.