[React.js] Как автоматически отменить асинхронный запрос при выгрузке компонента

Node.js внешний интерфейс JavaScript React.js
[React.js] Как автоматически отменить асинхронный запрос при выгрузке компонента

Введение

Проходя однажды мимо рабочей станции коллеги, я случайно увидел, как мой коллега пишет оценку интервью, и я увидел проблему: проблема с асинхронным запросом автоматически отменялась при удалении компонента, и это не удавалось.

Я:? ? ?

Поддерживает ли fetch запросы на прерывание вручную?

Поэтому я пошел в Интернет, чтобы искать все виды информации:how to abort fetch http request when component umounts

Тогда среди различных полученных данных тот, который, кажется, более надежен, это один:

componentDidMount(){
  this.mounted = true;

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

componentWillUnmount(){
  this.mounted = false;
}

Я:? ? ? ?

это все?

Однако это написание неверноabortТерятьfetchЗапрос просто не отвечает на результат после успешной выборки, что вообще не достигает цели отмены асинхронного запроса.

Поэтому я пошел спросить своих коллег, как на самом делеabortОтбросьте запрос на выборку, который уже был отправлен.

Мне коллега сказал: браузер пока не поддерживаетabortТерятьfetchпросить.

Я:……

Коллега продолжает: Но мы можем пройтиPromise.race([cancellation, fetch()])способ, сначала позвоните, прежде чем выборка действительно закончитсяcancellationметод для возвратаrejectПрямой конец этогоPromise, так что кажется, чтоabortотбросить отправляемую выборку, как для реальногоfetchНам не нужно заботиться о результате, потому что у нас естьrejectрезультат.

Я: Так есть ли вики с конкретными методами реализации?

Коллега: У нас это есть в коде, просто пойди и посмотри.

Я: ... (даже не знаю!)

Вот я и читал, и спрашивал, и внимательно изучал код, автоматически отменяющий асинхронный запрос при удалении компонента.

выполнить

Основная часть всего кода — это действительно строка кода, упомянутая коллегой только что:return Promise.race([cancellation, window.fetch(input, init)]);

Но здесьcancellationНа самом деле другойPromise,этоPromiseответственность за регистрациюabortСобытие, когда наш компонент выгружается, это активно срабатываетabortсобытие, так что, наконец, если компонент был размонтирован ранее,fetchНа запрос был дан ответ, просто следуйте обычной логике, иначе он вернет событие прерывания, потому что мы инициировали событие прерывания.rejectрезультат ответа.


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)]);
};

Так что, если мы вызовем этоabortА как же события, и на основании чего найти соответствующиеfetchзапрос?

Прежде всего, чтобы связать и вызвать наши пользовательские события, нам нужно реализовать набор классов Emitter, аналогичных тем, что есть в узле.Этот класс должен включать только события регистрации, события связывания и метод для запуска события.

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];
    }
  }
}

согласно сEmitterкласс, мы можем вывестиSignalкласс как маркерfetchкласс, затемSignalControllerкласс какSignalконтроллер класса.

abort-controller.js
class AbortSignal extends Emitter {
  constructor() {
    super();
    this.aborted = false;
  }
  toString() {
    return '[AbortSignal]';
  }
}

class AbortController {
  constructor() {
    super();
    this.signal = new AbortSignal();
  }
  abort() {
    this.signal.aborted = true;
    this.signal.dispatchEvent('abort');
  };
  toString() {
    return '[AbortController]';
  }
}

С помощью этих двух классов мы можем улучшить только чтоabortableFetchфункция.

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);
};

Добавляем параметр к входящему параметруsignalполе идентифицируетfetchЗапрос можно отменить, этоsignalлоготипSignalэкземпляр класса.

Затем, когда мы выгружаем автоматически триггерную сборкуAbortControllerизabortметод, вот и все.

Наконец, давайте переделыватьComponentКомпоненты со встроенными привязками для каждого компонентаsignalметод, который автоматически срабатывает при размонтировании компонентаabortметод.

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

/**
 * 用于组件卸载时自动cancel所有注册的promise
 */
export default class EnhanceComponent extends React.Component {
  constructor(props) {
    super(props);
    this.abortControllers = [];
  }
  componentWillUnmount() {
    this.abortControl();
  }

  /**
   * 取消signal对应的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 = [];
    }
  }

  /**
   * 注册control
   */
  bindControl = () => {
    const controller = new AbortController();
    this.abortControllers.push(controller);
    return controller.signal;
  }
  _findControl(signal) {
    const idx = this.abortControllers.findIndex(controller => controller.signal === signal);
    return idx;
  }
}

Таким образом, мы все наследуем отEnhanceComponentкомпоненты будут поставляться сbindControllerа такжеabortметод, мы будемbindControllerСгенерированоsignalПередача параметров fetch может завершить выгрузку компонента и автоматически отменить асинхронный запрос.

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(),
        })
    }
    // ...
}
Категории