background

One time passing by a colleague’s station, just see a colleague in writing interview evaluation, see there is a question: components uninstall automatically cancel asynchronous request problem, fail.

Me:??

Does fetch now support manual abort requests?

How to abort fetch HTTP request when component umounts

And then of all the different kinds of data that I got, the one that seemed to be more reliable was this one:

componentDidMount(){
  this.mounted = true;

  this.props.fetchData().then((response) = > {
    if(this.mounted) {
      this.setState({ data: response })
    }
  })
}

componentWillUnmount(){
  this.mounted = false;
}
Copy the code

Me: ????

Is that it?

However, this method does not abort the fetch request. It just does not respond to the fetch result after the fetch succeeds. This completely fails to cancel the asynchronous request.

So I asked a colleague how abort can really abort a fetch request that has already been sent.

A colleague told me that browsers do not support abort drop fetch requests.

I:…

Colleagues continue: Race ([Cancellation, fetch()])), which calls the Cancellation method to return a reject before the fetch actually ends, terminating the Promise. This makes it look like abort drops a fetch that is being sent, and we don’t need to deal with the actual fetch because we’ve got a reject.

Me: So is there a specific way to implement a wiki?

Colleague: We have it in our code, you just have to check it out.

I:… (I didn’t know that!)

So I read and asked, perusing the code that automatically cancels asynchronous requests for component uninstallation.

implementation

Return Promise.race([Cancellation, window.fetch(input, init)]);

The Cancellation is actually another Promise, which is responsible for registering an abort event. When our component is unloaded, the abort event is actively triggered. In this way, if the fetch request is completed before the component is unloaded, the normal logic will be followed. Otherwise, we fire abort and return a reject response.


const realFetch = window.fetch;
const abortableFetch = (input, init) = > {
    // Turn an event into a promise, reject it once `abort` is dispatched
    const cancellation = new Promise((_, reject) = > {
        init.signal.addEventListener(
            'abort',
            () => {
                reject(abortError);
            },
            { once: true}); });// Return the fastest promise (don't need to wait for request to finish)
    return Promise.race([cancellation, realFetch(input, init)]);
};
Copy the code

So how do we trigger this abort event and how do we find the corresponding FETCH request?

In order to bind and trigger our own custom events, we need to implement a set of classes similar to Those in Node. These classes only need to include registered events, binding events, and which methods trigger the events.

emitter.js
export default class Emitter {
  constructor() {
    this.listeners = {};
  }
  dispatchEvent = (type, params) = > {
    const handlers = this.listeners[type] || [];
    for(const handler of handlers) {
      handler(params);
    }
  }
  addEventListener = (type, handler) = > {
    const handlers = this.listeners[type] || (this.listeners[type] = []);
    handlers.push(handler);
  }
  removeEventListener = (type, handler) = > {
    const handlers = this.listeners[type] || [];
    const idx = handlers.indexOf(handler);
    if(idx ! = =- 1) {
      handlers.splice(idx, 1);
    }
    if(handlers.length === 0) {
      delete this.listeners[type]; }}}Copy the code

From the Emitter class we can derive a Signal class for marking fetch, and a SignalController class for Signal.

abort-controller.js
class AbortSignal extends Emitter {
  constructor() {
    super(a);this.aborted = false;
  }
  toString() {
    return '[AbortSignal]'; }}class AbortController {
  constructor() {
    super(a);this.signal = new AbortSignal();
  }
  abort() {
    this.signal.aborted = true;
    this.signal.dispatchEvent('abort');
  };
  toString() {
    return '[AbortController]'; }}Copy the code

Now that we have these two classes, we can improve the abortableFetch function.

abortable-fetch.js
if (typeof Symbol! = ='undefined' && Symbol.toStringTag) {
  // These are necessary to make sure that we get correct output for:
  // Object.prototype.toString.call(new AbortController())
  AbortController.prototype[Symbol.toStringTag] = 'AbortController';
  AbortSignal.prototype[Symbol.toStringTag] = 'AbortSignal';
}

const realFetch = window.fetch;
const abortableFetch = (input, init) = > {
  if (init && init.signal) {
    const abortError = new Error('Aborted');
    abortError.name = 'AbortError';
    abortError.isAborted = true;

    // Return early if already aborted, thus avoiding making an HTTP request
    if (init.signal.aborted) {
      return Promise.reject(abortError);
    }
    // Turn an event into a promise, reject it once `abort` is dispatched
    const cancellation = new Promise((_, reject) = > {
      init.signal.addEventListener(
        'abort',
        () => {
          reject(abortError);
        },
        { once: true}); });delete init.signal;

    // Return the fastest promise (don't need to wait for request to finish)
    return Promise.race([cancellation, realFetch(input, init)]);
  }

  return realFetch(input, init);
};
Copy the code

We add a signal field to the passed parameter to indicate that the fetch request can be cancelled. This signal is an instance of the Signal class.

The abort method of AbortController is then automatically fired when our component is unloaded, and that’s it.

Finally, we modify Component components so that each Component has a built-in signal binding method. The abort method is automatically triggered when the Component is unloaded.

enhance-component.js
import React from 'react';
import { AbortController } from 'lib/abort-controller';

/** * used to automatically cancel all registered Promises */ when the component uninstalls
export default class EnhanceComponent extends React.Component {
  constructor(props) {
    super(props);
    this.abortControllers = [];
  }
  componentWillUnmount() {
    this.abortControl();
  }

  /** * Cancel the Promise * @param {*} signal */
  abortControl(signal) {
    if(signal ! = =undefined) {
      const idx = this._findControl(signal);
      if(idx ! = =- 1) {
        const control = this.abortControllers[idx];
        control.abort();
        this.abortControllers.splice(idx, 1); }}else {
      this.abortControllers.forEach(control= > {
        control.abort();
      });
      this.abortControllers = []; }}/** * register control */
  bindControl = (a)= > {
    const controller = new AbortController();
    this.abortControllers.push(controller);
    return controller.signal;
  }
  _findControl(signal) {
    const idx = this.abortControllers.findIndex(controller= > controller.signal === signal);
    returnidx; }}Copy the code

In this way, all components that inherit from EnhanceComponent come with bindController and abort methods. We pass the signal generated by bindController into the fetch parameter to complete the component unload and automatically cancel the asynchronous request.

xxxComponent.js
import EnhanceComponent from 'components/enhance-component';
export default class Demo extends EnhanceComponent {
    // ...
    fetchData() {
        util.fetch(UPLOAD_IMAGE, {
            method: 'POST'.data: {},
            signal: this.bindControl(),
        })
    }
    // ...
}
Copy the code