By Tomasz Jakut

Translation: Crazy geek

Original text: ckeditor.com/blog/Aborti…

Reproduced without permission

Sometimes it can be difficult to perform asynchronous tasks, especially if a particular programming language does not allow you to cancel an operation that was started incorrectly or is no longer needed. Fortunately, JavaScript provides very convenient functionality to abort asynchronous activity. In this article, you can learn how to create abortable functions.

Abort signal

Shortly after the introduction of Promise into ES2015 and the emergence of Web apis that support new asynchronous solutions, the need to cancel asynchronous tasks arose. Initial attempts have focused on creating a common solution that is expected to become part of the ECMAScript standard in the future. However, discussions soon bogged down and could not resolve the issue. Therefore, the WHATWG prepared its own solution and introduced it directly into the DOM in the form of AbortController. The obvious drawback of this solution is that AbortController is not provided in Node.js, so there is no elegant or official way to cancel asynchronous tasks in this environment.

As you can see in the DOM specification, AbortController is described in a very general way. So you can use it in any kind of asynchronous API — even those that don’t yet exist. Currently only the Fetch API is officially supported, but you can use it in your own code!

Before we begin, let’s take a moment to analyze how AbortController works:

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

fetch( 'http://example.com', {
    signal: abortSignal / / 3
} ).catch( ( { message } ) = > { / / 5
    console.log( message ); }); abortController.abort();/ / 4
Copy the code

Looking at the code above, you can see that at the beginning you create a new instance of the AbortController DOM interface (1) and bind its signal property to the variable (2). Fetch () is then called and signal is passed as one of its options (3). To abort the resource, you simply call abortController.abort() (4). It will automatically reject the promise of fetch (), and the control will be passed to the Catch () block (5).

The signal property itself is very interesting and is the main star of the show. This property is an instance of the AbortSignal DOM interface that has the aborTED property, which contains information about whether the user has called the abortController.Abort () method. You can also bind abort event listeners to the event listener that is called when abortController.Abort () is called. In other words: AbortController is just the public interface to AbortSignal.

Terminable function

Suppose we use an asynchronous function to perform some very complex computation (for example, asynchronously processing data from a large array). For simplicity, the sample function simulates this by waiting five seconds and then returning the result:

function calculate() {
  return new Promise( ( resolve, reject ) = > {
    setTimeout( (a)= > {
      resolve( 1 );
    }, 5000); }); } calculate().then(( result ) = > {
  console.log( result ); });Copy the code

But sometimes users want to be able to abort this costly operation. Yes, they should be able to. Add a button to start and stop calculations:

<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>Copy the code

In the code above, add an asynchronous click event listener to button (1) and call the Calculate () function (2) from there. After five seconds, an alert dialog box (3) with the results will be displayed. In addition, script [type = module] is used to force JavaScript code into strict mode — because it is more elegant than the ‘use strict’ compilation directive.

Now add the ability to abort asynchronous tasks:

{ / / 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( (a)= > {
        resolve( 1 );
      }, 5000 );

      abortSignal.addEventListener( 'abort', () = > {/ / 6
        const error = new DOMException( 'Calculation aborted by the user'.'AbortError' );

        clearTimeout( timeout ); / / 7
        reject( error ); / / 8}); }); }}Copy the code

As you can see, the code gets longer. But there’s no reason to panic, it’s not getting any harder to understand!

Everything is contained in block (1), which is the IIFE equivalent. Therefore, the abortController variable (2) does not leak into the global scope.

First, set its value to NULL. This value changes when the mouse clicks on the button. Then set its value to a new instance of AbortController (3). After that, pass the instance’s signal attribute directly to your Calculate () function (4).

If the user clicks the button again within five seconds, abortController.abort() (5) is called. This, in turn, triggers the Abort event (6) on the AbortSignal instance you previously passed to Calculate ().

Inside the ABORT event listener, the tick timer (7) is removed and the promise with the appropriate error is rejected (8; According to the specification, it must be DOMException of type ‘AbortError’). This error ultimately passes control to the catch (9) and finally block (10).

You should also prepare code to handle the following cases:

const abortController = new AbortController();

abortController.abort();
calculate( abortController.signal );
Copy the code

In this case, the abort event will not be fired because it occurs before the signal is passed to the calculate() function. So you should do some refactoring:

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( (a)= > {
      resolve( 1 );
    }, 5000 );

    abortSignal.addEventListener( 'abort', () => { clearTimeout( timeout ); reject( error ); }); }); }Copy the code

The error is moved to the top (1). Therefore, you can reuse it in different parts of your code (however, it’s more elegant to create an error factory, as silly as it sounds). Abortsignal.aborted (2) is checked for a protection clause. If equal to true, the Calculate () function will reject promises with appropriate errors without doing anything else.

This is how you create fully abortable asynchronous functions. Demo can get here (blog.com andeer. Pl/assets/I – ci…

Welcome to pay attention to the front end public number: front end pioneer, free front-end engineering utility kit.