· Zen HuiFer · Learn  · 需要8 分钟阅读

This JavaScript API is more powerful than you imagine!

Delve into the powerful, yet overlooked, JavaScript API AbortController for enhanced control over asynchronous tasks. Use it to gracefully cancel fetch requests, manage event listeners, and more without cumbersome try-catch blocks.

Delve into the powerful, yet overlooked, JavaScript API AbortController for enhanced control over asynchronous tasks. Use it to gracefully cancel fetch requests, manage event listeners, and more without cumbersome try-catch blocks.

This JavaScript API is more powerful than you imagine!

Today, let’s talk about a powerful standard JavaScript API that may have been overlooked by you-AbortController

In the past, people mentionedAbortControllerWhen it comes to interrupt requests, examples are usually given, and even the description given by MDN is like this:

howeverAbortControllerThe ability is not limited to this,AbortControllerIt is a global class in JavaScript that can be used to terminate any asynchronous operation. The usage method is as follows:

const controller = new AbortController();controller.signal;
controller.abort();

We create aAbortControllerAfter the instance, two things will be obtained:

  • signalAttribute, this is aAbortSignalFor instance, we can pass it to the API that needs to be interrupted to respond to the interrupt event and handle it accordingly, for example, by passing it tofetch()The method can terminate this request;

  • .abort()Method, calling this method will triggersignalSuspend the event and mark the signal as aborted.

We can monitor through surveillanceabortEvent, then implement termination based on specific logic:

controller.signal.addEventListener('abort', () => {
      //Implement termination logic    
});

Let’s learn about some support for facial cleansingAbortSignalThe standard JavaScript API.

usage

Event Monitor

We can provide a pause when adding event listenerssignalIn this way, when the termination occurs, the listener will automatically delete.

const controller = new AbortController();window.addEventListener('resize', listener, { signal: controller.signal });controller.abort();

If we callcontroller.abort()Will come fromwindowDelete in the middleresizemonitor. This is a very elegant way to handle event listeners, as we no longer need abstract listener functions to call themremoveEventListener()

// const listener = () => {}
// window.addEventListener('resize', listener)
// window.removeEventListener('resize', listener)const controller = new AbortController();
window.addEventListener('resize', () => {}, { signal: controller.signal });
controller.abort();

If different parts of the application are responsible for deleting listeners, pass aAbortControllerInstances would be more convenient, and then I found that I could use a single onesignalDelete multiple event listeners!

useEffect(() => {
  const controller = new AbortController();  window.addEventListener('resize', handleResize, {
    signal: controller.signal,
  });
  window.addEventListener('hashchange', handleHashChange, {
    signal: controller.signal,
  });
  window.addEventListener('storage', handleStorageChange, {
    signal: controller.signal,
  });  return () => {
         //Calling `. abort() ` will delete all associated event listeners     
    controller.abort();
  };
}, []);

In the above example, I added auseEffect()Hooks, which introduce event listeners with different purposes and logic. Then, in the cleaning function, I only need to call it oncecontroller.abort()You can delete all added listeners, it’s still very useful!

Fetch request

fetch()Functions also supportAbortSignalInterrupt requests should also be includedAbortControllerThe most frequently used scenario.

oncesignalUp thereabortThe event is triggered,fetch()Request returned by functionPromiseIt will be rejected, thereby terminating unfinished requests.

   <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Example of file upload</title>
</head>
<body>
  <input type="file" id="fileInput" />
  <button id="uploadButton">upload</button>
  <button id="cancelButton">Cancel upload</button>  <script>
    function uploadFile(file) {
      const controller = new AbortController();             //Pass the abort signal to the fetch request   
      const response = fetch('/upload', {
        method: 'POST',
        body: file,
        signal: controller.signal,
      });          return { response, controller };
    }    document.getElementById('uploadButton').addEventListener('click', () => {
      const fileInput = document.getElementById('fileInput');
      const file = fileInput.files[0];      if (!file) {
        alert(   Please select a file   );
        return;
      }      const { response, controller } = uploadFile(file);      response.then(res => res.json())
        .then(data => {
          console.log(   File upload successful:   , data);
        })
        .catch(err => {
          if (err.name === 'AbortError') {
            console.log(   'File upload canceled'   );
          } else {
            console.error(   File upload failed:   , err);
          }
        });         //Save controller to cancel upload   
      window.currentUploadController = controller;
    });    document.getElementById('cancelButton').addEventListener('click', () => {
      if (window.currentUploadController) {
        window.currentUploadController.abort();
        console.log(   I clicked the cancel upload button   );
      } else {
        console.log(   There is no ongoing upload operation   );
      }
    });</script>
</body>
</html>   

In the example aboveuploadFile()The function initiated aPOSTRequest, return associatedresponsePromise and oneAbortControllerFor example, when the user clicks the cancel upload button, we canAbortControllerThe instance can terminate this request at any time.

In Node.js, it is composed ofhttpThe requests sent by the module are also supportedsignalAttribute!

const http = require('http');
const { AbortController } = require('abort-controller');function makeRequest() {
  const controller = new AbortController();  const options = {
    hostname: 'example.com',
    port: 80,
    path: '/',
    method: 'GET',
         //Pass AbortSignal to the request     
    signal: controller.signal
  };  const req = http.request(options, (res) => {
    let data = '';
    res.on('data', (chunk) => {
      data += chunk;
    });    res.on('end', () => {
      console.log('Response:', data);
    });
  });  req.on('error', (e) => {
    if (e.name === 'AbortError') {
      console.log(     Request cancelled     );
    } else {
      console.error(     `Request encountered an issue:${e.message}`     );
    }
  });  req.end();       //Simulate cancellation operations, such as canceling requests after 2 seconds     
  setTimeout(() => {
    controller.abort();
  }, 2000);
}makeRequest();

AbortSignalStatic methods of classes

AbortSignalClasses also have some static methods that can simplify request processing in JavaScript.

AbortSignal.timeout

We can useAbortSignal.timeout()As a shortcut, a static method creates a signal that triggers an abort event after a certain timeout period. If you only want to cancel a request after it expires, you don’t need to create oneAbortControllerAnd now:

    document.getElementById('fetchButton').addEventListener('click', () => {
      const url = 'https://jsonplaceholder.typicode.com/posts/1';     //Example API Address          fetch(url, {
             //If the request exceeds 1700 milliseconds, it will automatically terminate     
        signal: AbortSignal.timeout(1700),
      })
      .then(response => {
        if (!response.ok) {
          throw new Error(     Network response failed     );
        }
        return response.json();
      })
      .then(data => {
        console.log(     Request successful:     , data);
      })
      .catch(error => {
        if (error.name === 'AbortError') {
          console.error(     Request timeout canceled:     , error);
        } else {
          console.error(     Request error:     , error);
        }
      });
    });

AbortSignal.any

be similar toPromise.race()We can use the method of handling multiple promisesAbortSignal.any()The static method combines multiple abort signals into one. Here is a specific example:

   <!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Example of AbortSignal.any</title>
</head>
<body>
  <button id="stopButton">Stop monitoring</button>  <script>
    const publicController = new AbortController();
    const internalController = new AbortController();       //Create WebSocket connection   
    const socket = new WebSocket('wss://conardli.websocket.org');       //Triggered when WebSocket connection is opened   
    socket.addEventListener('open', () => {
      console.log(   WebSocket connection established   );
      socket.send('Hello WebSocket!');
    });       //Process received messages   
    function handleMessage(event) {
      console.log(   Received message:   , event.data);
    }       //Combine multiple abort signals into one using AbortSignal.any   
    socket.addEventListener('message', handleMessage, {
      signal: AbortSignal.any([publicController.signal, internalController.signal]),
    });       //Simulate cancellation operation   
    document.getElementById('stopButton').addEventListener('click', () => {
      publicController.abort();
      console.log(   Stop monitoring message events   );
    });       //It can also be cancelled through the internal controller   
    setTimeout(() => {
      internalController.abort();
      console.log(   Internal controller automatically suspends monitoring   );
    }, 5000);</script>
</body>
</html>   
  1. Created twoAbortControllerExamples, namelypublicControllerandinternalController

  2. Establish a connection using WebSocket and send a message after the connection is established.

  3. For WebSocketmessageAdd listeners to the event and use them throughAbortSignal.anyCombine two stop signals.

  4. Added a button on the page that will be called when clickedpublicController.abort()Stop monitoring message events.

  5. Additionally, usingsetTimeoutSimulated an internal controller that automatically terminates the listening operation after 5 seconds.

In this way, multiple abort signals can be flexibly combined, and when any one signal is triggered, the relevant event listener will be canceled.

Cancel Flow

We can still use itAbortControllerandAbortSignalTo cancel the flow.

In the following example, we create aWritableStreamAnd through monitoringcontroller.signalofabortEvents are used to handle the termination of flow operations.

   <!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Example of canceling flow operation</title>
</head>
<body>
  <button id="cancelButton">Cancel write operation</button>  <script>
    async function example() {
      const abortController = new AbortController();      const stream = new WritableStream({
        write(chunk, controller) {
          console.log(   Writing:   , chunk);             //Monitor stop signal   
          controller.signal.addEventListener('abort', () => {
            console.log(   The write operation has been canceled   );
               //Process flow termination logic, such as clearing resources or notifying users   
          });
        },
        close() {
          console.log(   'Write completed'   );
        },
        abort(reason) {
          console.warn(   Write aborted:   , reason);
        }
      });      const writer = stream.getWriter();         //Simulate write operation   
      writer.write(   Data Block 1   );
      writer.write(   Data Block 2   );         //Save abortController for canceling operation   
      window.currentAbortController = abortController;
      writer.releaseLock();    //Release the write lock before creating a new write operation            //Monitor the click event of the cancel button   
      document.getElementById('cancelButton').addEventListener('click', async () => {
        if (window.currentAbortController) {
          await writer.abort();
          window.currentAbortController.abort();
          console.log(   I clicked the cancel write operation button   );
        } else {
          console.log(   There are no ongoing write operations   );
        }
      });
    }    example();</script>
</body>
</html>   

WritableStreamThe controller has been exposedsignalAttribute, i.e. the sameAbortSignal. This way, I can callwriter.abort()This will flow inwrite()In the methodcontroller.signalAn upward bubble triggers an abort event.

Suspend any logic

actuallyAbortControllerThe ability is not limited to this, we can use it to make any logic interruptible!

For example, in the following example, we willAbortControllerAdding to Drizzle ORM transactions allows us to cancel multiple transactions at once.

import { TransactionRollbackError } from 'drizzle-orm';function makeCancelableTransaction(db) {
  return (callback, options = {}) => {
    return db.transaction((tx) => {
      return new Promise((resolve, reject) => {
        options.signal?.addEventListener('abort', async () => {
          reject(new TransactionRollbackError());
        });        return Promise.resolve(callback.call(this, tx)).then(resolve, reject);
      });
    });
  };
}

makeCancelableTransaction()The function accepts a database instance, returns a high-order transaction function, and can then accept an abortsignalAs a parameter.

BysignalAdd on instanceabortThe listener of the event, I can know when the termination occurred. This event listener will be called when the termination event is triggered, that is, whencontroller.abort()When called. Therefore, when a termination occurs, I can return aTransactionRollbackErrorError to roll back the entire transaction (this is equivalent to callingtx.rollback()And throw the same error).

Then, we use it in Drizzle.

const db = drizzle(options);const controller = new AbortController();
const transaction = makeCancelableTransaction(db);await transaction(
  async (tx) => {
    await tx
      .update(accounts)
      .set({ balance: sql`${accounts.balance} - 100.00` })
      .where(eq(users.name, 'Dan'));
    await tx
      .update(accounts)
      .set({ balance: sql`${accounts.balance} + 100.00` })
      .where(eq(users.name, 'Andrew'));
  },
  { signal: controller.signal }
);

We calledmakeCancelableTransaction()Tool function, and pass it indbCreate a custom interruptible transaction using an instance. From now on, I can use this custom transaction to perform multiple database operations as usual in Drizzle, and also provide an abort for itsignalCancel all operations at once.

Abort error handling

Each termination event is accompanied by a termination reason, which allows us to have more customization and respond differently to different termination reasons.

The reason for termination iscontroller.abort()Optional parameters for the method. You can do it at any timeAbortSignalExamples ofreasonReason for access termination in attribute.

    async function fetchData() {
      const controller = new AbortController();
      const signal = controller.signal;           //Monitor the abort event and print the reason for the abort     
      signal.addEventListener('abort', () => {
        console.log(     Reason for request termination:     , signal.reason);      //Print custom termination reason     
      });      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts/1', { signal });
        const data = await response.json();
        console.log(     Request successful:     , data);
      } catch (error) {
        if (error.name === 'AbortError') {
          console.error(     Request cancelled due to suspension:     , error.message);
        } else {
          console.error(     Request error:     , error.message);
        }
      }                 //Save controller for canceling operation     
      window.currentAbortController = controller;
    }    fetchData();         //Monitor the click event of the cancel button     
    document.getElementById('cancelButton').addEventListener('click', () => {
      if (window.currentAbortController) {
        window.currentAbortController.abort(     The user cancelled the request     );       //Provide custom termination reasons     
        console.log(     I clicked the cancel request button     );
      } else {
        console.log(     There are no ongoing requests     );
      }
    });

reasonParameters can be any JavaScript value, so we can pass strings, errors, and even objects.

compatibility

AbortControllerThe compatibility is very good, and it has been included in the Web Compatibility Baseline for a long time. Since March 2019, it can be used in all mainstream browsers.

返回博客
New package in Go 1.23: unique

New package in Go 1.23: unique

Go 1.23 introduces unique package for value normalization, enhancing memory efficiency and equality checks. Learn how "interning" works with unique and its benefits for Go developers.

How to cache well in Go

How to cache well in Go

Optimize Go app performance with caching strategies. Learn local vs distributed cache, memory management, and eviction policies. Enhance efficiency with Go's new unique package.

The noCopy strategy you should know in Golang

The noCopy strategy you should know in Golang

Discover the importance of noCopy in Golang development. Learn how it prevents accidental copying of critical structures like sync primitives. Enhance your Go code safety and efficiency.