背景介紹
某次路過同事的工位,剛好看到同事在寫面試評價,看到裡面有一個問題:元件解除安裝時自動取消非同步請求問題,不及格。
我:???
現在fetch已經支援手動abort請求了嗎?
於是上網去查各種資料: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
請求,只是不去響應fetch成功之後的結果而已,這完全沒有達到取消非同步請求的目的。
於是我去問了問同事,如何真正abort
掉一個已經傳送出去的fetch請求。
同事跟我說:現在瀏覽器還不支援abort
掉fetch
請求。
我:……
同事繼續:不過我們可以通過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
類的例項。
然後當我們元件解除安裝的時候自動觸發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(),
})
}
// ...
}
複製程式碼