Як скасувати асинхронне завдання в JavaScript

8 хв. читання

Обробка асинхронних завдань може бути проблемною, особливо якщо мова програмування, з якою ви працюєте, не підтримує такий механізм. На щастя, JavaScript підтримує. Розглянемо його детальніше, а для закріплення інформації створимо власну функцію скасування асинхронних завдань.

Abort Signal

Потреба скасовувати асинхронні таски виникла, щойно в ES2015 з'явився об'єкт Promise, а також після появи декількох Web API з підтримкою асинхронності. Спочатку розробники прагнули створити універсальний механізм, який пізніше міг би потрапити до стандарту ECMAScript. Однак це загальне рішення знайти так і не вдалося.

Саме тому на допомогу приходить WHATWG з власною пропозицією — AbortController, який працює на базі DOM. Очевидний недолік — AbortController не функціонує в Node.js, тобто все середовище залишається без офіційного механізму скасування асинхронних завдань.

У специфікації DOM AbortController описаний досить загально. Зважаючи на це, ви можете використовувати його у будь-якому асинхронному API, навіть неофіційному. На момент написання матеріалу лише Fetch API офіційно підтримував механізм скасування, але ніщо не заважає вам застосувати AbortController у власних рішеннях.

Перш ніж ми перейдемо до власної реалізації скасування, розглянемо детальніше принцип роботи AbortController:

const abortController = new AbortController(); // 1
const abortSignal = abortController.signal; // 2

fetch( 'https://web.archive.org/web/20230605173144/http://example.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 і є головним героєм нашого матеріалу. Це екземпляр AbortController інтерфейсу DOM, який має властивість aborted з інформацією про виклик методу abortController.abort(). Ви також можете створити слухача події abort, який очікує виклику abortController.abort(). Тобто AbortController — це лише публічний інтерфейс для AbortSignal.

Функція зі скасуванням

Уявімо, що у нас є асинхронна функція, яка виконує дуже складні обчислення (наприклад, асинхронно обробляє дані великого масиву). Для нашого прикладу тестова функція буде симулювати важкі обчислення за допомогою таймера, який повертатиме результат через 5 секунд:

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 = 'Зупинити обчислення';

    const result = await calculate(); // 2

    alert( result ); // 3

    target.innerText = 'Обчислити';
  } );

  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 = 'Обчислити';

      return;
    }

    abortController = new AbortController(); // 3
    target.innerText = 'Зупинити обчислення';

    try {
      const result = await calculate( abortController.signal ); // 4

      alert( result );
    } catch {
      alert( 'Навіщо ви це зробили?!' ); // 9
    } finally { // 10
      abortController = null;
      target.innerText = 'Обчислити';
    }
  } );

  function calculate( abortSignal ) {
    return new Promise( ( resolve, reject ) => {
      const timeout = setTimeout( ()=> {
        resolve( 1 );
      }, 5000 );

      abortSignal.addEventListener( 'abort', () => { // 6
        const error = new DOMException( 'Обчислення скасоване користувачем', 'AbortError' );

        clearTimeout( timeout ); // 7
        reject( error ); // 8
      } );
    } );
  }
}

Як бачимо, наш код стає дедалі довшим, однак залишається легким для розуміння.

Блок коду (1) працює подібно до IIFE. Тут змінна AbortController (2) не потрапляє в глобальну область видимості.

Спершу ми ініціалізуємо її як null. Це значення змінюється після кліку на кнопку: створюється новий екземпляр AbortController (3). Далі передаємо властивість signal щойно створеного екземпляра одразу до нашої функції calculate() (4).

Якщо користувач натисне на кнопку ще раз, перш ніж мине 5 секунд, виконається abortController.abort() (5), тобто в екземпляра AbortSignal, який ми передали до calculate() (6), відбувається подія abort.

Всередині слухача події abort ми очищаємо таймер (7) та відхиляємо проміс з відповідною помилкою (8). Згідно зі специфікацією, це має бути DOMException з типом AbortError. Оскільки ми явно повернули помилку, управління передається до блоків catch та 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( 'Обчислення скасоване користувачем', '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() відхиляє проміс з відповідною помилкою без виконання додаткових кроків.

Ось і все. Ми щойно створили асинхронну функцію з підтримкою механізму скасування. Результат за посиланням.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.8K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація