[React.js]組件卸載如何自動取消異步請求

背景介紹

某次路過同事的工位,恰好看到同事在寫面試評價,看到裏面有一個問題:組件卸載時自動取消異步請求問題,不及格。node

我:???react

如今fetch已經支持手動abort請求了嗎?面試

因而上網去查各類資料:how to abort fetch http request when component umountspromise

而後獲得的各類各樣的資料裏面,看起來比較靠譜的是這樣一種:瀏覽器

componentDidMount(){
  this.mounted = true;

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

componentWillUnmount(){
  this.mounted = false;
}
複製代碼

我:????異步

就這樣嗎?函數

然而這個寫法並無真的abortfetch請求,只是不去響應fetch成功以後的結果而已,這徹底沒有達到取消異步請求的目的。fetch

因而我去問了問同事,如何真正abort掉一個已經發送出去的fetch請求。ui

同事跟我說:如今瀏覽器還不支持abortfetch請求。this

我:……

同事繼續:不過咱們能夠經過Promise.race([cancellation, fetch()])的方式,在fetch真正結束以前先調用cancellation方法來返回一個reject,直接結束這個Promise,這樣就能夠看似作到abort掉一個正在發送的fetch,至於真正的fetch結果是怎麼怎樣的咱們就不須要管了,由於咱們已經獲得了一個reject結果。

我:那麼有具體實現方法的wiki嗎?

同事:咱們代碼裏面就有,你去看看就行。

我:……(我居然不知道!)

因而我就連讀帶問,認真研讀了一下組件卸載自動取消異步請求的代碼。

實現

整個代碼的核心部分確實是剛纔同事提到的那一行代碼:return Promise.race([cancellation, window.fetch(input, init)]);

不過這裏的cancellation實際上是另外一個Promise,這個Promise負責註冊一個abort事件,當咱們組件卸載的時候,主動觸發這個abort事件,這樣最後若是組件卸載以前,fetch請求已經響應完畢,就走正常邏輯,不然就由於咱們觸發了abort事件返回了一個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請求呢?

首先爲了綁定和觸發咱們自定義的事件,咱們須要本身實現一套相似node裏面的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類的實例。

而後當咱們組件卸載的時候自動觸發AbortControllerabort方法,就能夠了。

最後咱們改造一下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的組件都會自帶一個bindControllerabort方法,咱們將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(),
        })
    }
    // ...
}
複製代碼
相關文章
相關標籤/搜索