#javascript #reactjs #async-await #promise #cancellation
#javascript #reactjs #асинхронное ожидание #обещание #отмена
Вопрос:
Я хочу отменить обещание в моем приложении React, используя AbortController
и, к сожалению abort event
, не распознается, поэтому я не могу на него реагировать.
Моя настройка выглядит так:
WrapperComponent.tsx: здесь я создаю AbortController и передаю сигнал своему методу 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'));
});
});
};
В моем компоненте таблицы я вызываю abort()
метод следующим образом:
export const Table = ({controller}) => {
const handleAbort = ( fileName: string) => {
controller.abort();
};
return (
<Button
onClick={() => handleAbort()}
/>
);
}
Что я здесь делаю не так? Моя консоль.журналы не отображаются, и значение signal
никогда не устанавливается true
после вызова handleAbort
обработчика.
Комментарии:
1. Где
doSomeStuff
вызывается? Вы уверены, что он не вызывается до установки сигнала прерывания?2. Это обещание будет выполнено мгновенно . как только он будет разрешен, отклонение его ничего не даст. Какая здесь асинхронная задача?
3. @sma метод doSomeStuff вызывается в моем перехватчике useEffect. Мне пришлось упростить код здесь: (
4. Вызывается @parktomatomi в цикле for -> await hashChunk(фрагмент, хешер); . Вычисленное значение будет добавлено к хеш-константе, которая определена ниже цикла for . Извините, мне пришлось сильно упростить код: (
5. Я спрашиваю, потому что «hashChunk» звучит как ресурсоемкий метод, в котором вы перебираете некоторые данные и вычисляете хэш. Если вы не выполняете свою работу внутри этого конструктора обещаний или вообще не выпускаете поток, используя
await
,Worker
, или каким-либо другим способом, он на самом деле не будет асинхронным. Он просто выполнит работу и вернет обещание, которое уже выполнено.
Ответ №1:
На основе вашего кода необходимо внести несколько исправлений:
Не возвращайте new Promise()
внутри async
функции
Вы используете new Promise
, если берете что-то основанное на событиях, но естественно асинхронное, и оборачиваете это в обещание. Примеры:
- setTimeout
- Сообщения веб-работника
- События FileReader
Но в асинхронной функции возвращаемое значение уже будет преобразовано в обещание. Отклонения будут автоматически преобразованы в исключения, которые вы можете перехватить с 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
}
}
Вызывающий получит обещание сразу, но оно не будет разрешено, пока код не попадет в оператор 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, является совместной процедурой, нет упреждающей многозадачности, когда вы можете прервать поток или принудительно отклонить. Вместо этого вы можете создать простой объект token и передать его своей длительно работающей асинхронной функции:
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 возвращают обещания. Но на самом деле это работает только await
в загрузчике WASM. Это кэшируется после первого запуска, и после этого все вычисления выполняются синхронно.
Асинхронный код, который await
на самом деле не имеет никаких преимуществ. Он не будет останавливаться, чтобы разблокировать поток.
Итак, как вы можете позволить своему коду дышать, если у вас есть код с интенсивным использованием процессора, подобный тому, который использует hash-wasm? Вы можете выполнять свою работу постепенно и планировать эти приращения с помощью 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, которые позволяют выполнять тяжелую для процессора работу вне потока, чтобы не блокировать основной поток. Библиотеки, такие как workerize, могут помочь вам преобразовать асинхронные функции для запуска в worker.
Это все, что у меня есть на данный момент, прошу прощения за написание романа
Комментарии:
1. Хороший роман, хороший контент! 😛
2. Вау! Спасибо за этот подробный ответ. Я удалил AbortController. Есть некоторые сообщения, которые не только используют контроллер с выборкой, но в моем случае это просто не сработало. Затем я создал свой собственный токен, как вы описали — теперь он работает :). Спасибо!
Ответ №2:
Я могу предложить свою библиотеку (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>
);
}