背景介绍
某次路过同事的工位,刚好看到同事在写面试评价,看到里面有一个问题:组件卸载时自动取消异步请求问题,不及格。
我:???
现在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(), }) } // ...}复制代码