某次路過同事的工位,恰好看到同事在寫面試評價,看到裏面有一個問題:組件卸載時自動取消異步請求問題,不及格。node
我:???react
如今fetch已經支持手動abort請求了嗎?面試
因而上網去查各類資料:how to abort fetch http request when component umounts
promise
而後獲得的各類各樣的資料裏面,看起來比較靠譜的是這樣一種:瀏覽器
componentDidMount(){
this.mounted = true;
this.props.fetchData().then((response) => {
if(this.mounted) {
this.setState({ data: response })
}
})
}
componentWillUnmount(){
this.mounted = false;
}
複製代碼
我:????異步
就這樣嗎?函數
然而這個寫法並無真的abort
掉fetch
請求,只是不去響應fetch成功以後的結果而已,這徹底沒有達到取消異步請求的目的。fetch
因而我去問了問同事,如何真正abort
掉一個已經發送出去的fetch請求。ui
同事跟我說:如今瀏覽器還不支持abort
掉fetch
請求。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類,這個類只須要包含註冊事件,綁定事件以及觸發事件是哪一個方法便可。
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
類的控制器。
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
函數了。
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
方法。
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的參數就能夠完成組件卸載是自動取消異步請求了。
import EnhanceComponent from 'components/enhance-component';
export default class Demo extends EnhanceComponent {
// ...
fetchData() {
util.fetch(UPLOAD_IMAGE, {
method: 'POST',
data: {},
signal: this.bindControl(),
})
}
// ...
}
複製代碼