Обработка асинхронных задач в программе часто утомительна, особенно в языках программирования, которые не поддерживают отмену асинхронных задач. К счастью, JavaScript предоставляет очень удобный механизм отмены асинхронных задач.
сигнал прерывания
С момента введения ES2015Promise, разработчикам необходимо отменять асинхронные задачи, и некоторые веб-API, представленные позже, также начали поддерживать асинхронные решения, такие как Fetch API. Комитет TC39 (то есть организация, разработавшая стандарт ECMAScript) первоначально попытался определить общее решение для последующего использования в качестве стандарта ECMAScript. Однако после того, как обсуждение не привело ни к какому выводу, вопрос был отложен. Ввиду этого WHATWG (Организация по разработке стандартов HTML) начала заново и сама придумала решение, которое напрямую представило стандарт DOM. AbortController. Недостаток этого подхода очевиден, потому что это не стандарт ECMAScript на уровне языка, поэтому платформа Node.js не поддерживаетAbortController.
В спецификации DOMAbortControllerОн разработан, чтобы быть очень универсальным, поэтому вы можете использовать его практически в любом асинхронном API. В настоящее время официально поддерживается только Fetch API, но вы можете использовать его в своем собственном асинхронном коде.
Прежде чем мы начнем, давайте посмотрим наAbortControllerКак это работает:
const abortController = new AbortController(); // 1
const abortSignal = abortController.signal; // 2
fetch( 'http://kaysonli.com', {
signal: abortSignal // 3
} ).catch( ( { message } ) => { // 5
console.log( message );
} );
abortController.abort(); // 4
Приведенный выше код очень прост, сначала создайтеAbortControllerэкземпляр (1) и преобразует егоsignalСвойство присваивается переменной (2). тогда позвониfetch()и пройти вsignalпараметр (3). Вызывается при отмене запросаabortController.abort()(4). Это автоматически выполнитfetch()отклонить, то есть ввестиcatch()Раздел (5).
этоsignalАтрибуты лежат в основе. СобственностьAbortSignalDOM-интерфейсэкземпляр, который имеетabortedатрибут, с тем, следует ли вызыватьabortController.abort()связанная информация. Вы также можете контролировать на немabortсобытие, которое находится вabortController.abort()Уволен, когда его позвали. Проще говоря,AbortController то естьAbortSignalобщедоступный интерфейс .
Отменяемая функция
Предположим, есть асинхронная функция, выполняющая сложный расчет, для простоты смоделируем ее таймером:
function calculate() {
return new Promise( ( resolve, reject ) => {
setTimeout( ()=> {
resolve( 1 );
}, 5000 );
} );
}
calculate().then( ( result ) => {
console.log( result );
} );
Возможно, пользователь захочет отменить эту трудоемкую задачу. Запускаем и останавливаем кнопкой:
<button id="calculate">Calculate</button>
<script type="module">
document.querySelector( '#calculate' ).addEventListener( 'click', async ( { target } ) => { // 1
target.innerText = 'Stop calculation';
const result = await calculate(); // 2
alert( result ); // 3
target.innerText = 'Calculate';
} );
function calculate() {
return new Promise( ( resolve, reject ) => {
setTimeout( ()=> {
resolve( 1 );
}, 5000 );
} );
}
</script>
Приведенный выше код привязывает асинхронную кнопку кclickОбработчик события (1) и вызывается внутриcalculate()функция (2). Через 5 секунд появится диалоговое окно с результатом (3). Кстати,script[type=module]Вы можете перевести код JavaScript в строгий режим, следуя'use strict'тот же эффект.
Добавьте функцию прерывания асинхронных задач:
{ // 1
let abortController = null; // 2
document.querySelector( '#calculate' ).addEventListener( 'click', async ( { target } ) => {
if ( abortController ) {
abortController.abort(); // 5
abortController = null;
target.innerText = 'Calculate';
return;
}
abortController = new AbortController(); // 3
target.innerText = 'Stop calculation';
try {
const result = await calculate( abortController.signal ); // 4
alert( result );
} catch {
alert( 'WHY DID YOU DO THAT?!' ); // 9
} finally { // 10
abortController = null;
target.innerText = 'Calculate';
}
} );
function calculate( abortSignal ) {
return new Promise( ( resolve, reject ) => {
const timeout = setTimeout( ()=> {
resolve( 1 );
}, 5000 );
abortSignal.addEventListener( 'abort', () => { // 6
const error = new DOMException( 'Calculation aborted by the user', 'AbortError' );
clearTimeout( timeout ); // 7
reject( error ); // 8
} );
} );
}
}
Код стал намного длиннее, но не паникуйте, его не так уж сложно понять.
Самый внешний блок кода (1) эквивалентен IIFE (немедленно выполняемому функциональному выражению), так что переменнаяabortController(2) Это не загрязнит общую ситуацию.
Сначала установите его значение наnull, и его значение изменяется при нажатии кнопки. Затем назначьте его какAbortControllerэкземпляр (3), затем поместите экземплярsignalсвойства передаются непосредственно вcalculate()функция (4).
Если пользователь снова нажмет кнопку в течение 5 секунд, она будет выполненаabortController.abort()функция (5). Это будет просто переданоcalculate()изAbortSignalтриггер на экземпляреabortсобытие (6).
существует abortОбработчик события очищает таймер (7), затем отклоняет обещание (8) с соответствующим объектом исключения.
Согласно спецификации DOM, этот объект исключения должен быть
'AbortError'ТипDOMException.
Объект исключения, переданный в финалcatch(9) иfinally(10).
Но также рассмотрим эту ситуацию:
const abortController = new AbortController();
abortController.abort();
calculate( abortController.signal );
В этой ситуации abortСобытие не запускается, потому что оно находится вsignalперейти кcalculate()до выполнения функции. Для этого нам нужно изменить следующий код:
function calculate( abortSignal ) {
return new Promise( ( resolve, reject ) => {
const error = new DOMException( 'Calculation aborted by the user', 'AbortError' ); // 1
if ( abortSignal.aborted ) { // 2
return reject( error );
}
const timeout = setTimeout( ()=> {
resolve( 1 );
}, 5000 );
abortSignal.addEventListener( 'abort', () => {
clearTimeout( timeout );
reject( error );
} );
} );
}
Определение объекта исключения было перемещено вверх (1), чтобы его можно было повторно использовать в двух местах. Кроме того, есть дополнительное условное решениеabortSignal.aborted(2). если его значениеtrue,calculate()Функция должна немедленно отклонить промис, нет необходимости продолжать дальше.
На данный момент мы реализовали полностью отменяемую асинхронную функцию, которая может пригодиться в будущем, когда нам понадобится обрабатывать асинхронные задачи.
Увидев этот довольно темпераментный логотип, почему вы не обращаете внимания?