从 axios 源码中了解到的 Promise 链与请求的取消
axios 中一个请求取消的示例: axios 取消请求的示例代码import React, { useState, useEffect } from "react";
import axios, { AxiosResponse } from "axios";
export default function App() {
const [index, setIndex] = useState(0);
const [imgUrl, setImgUrl] = useState("");
useEffect(() => {
console.log(`loading ${index}`);
const source = axios.CancelToken.source();
axios
.get("https://dog.ceo/api/breeds/image/random", {
cancelToken: source.token
})
.then((res: AxiosResponse<{ message: string; status: string }>) => {
console.log(`${index} done`);
setImgUrl(res.data.message);
})
.catch(err => {
if (axios.isCancel(source)) {
console.log(err.message);
}
});
return () => {
console.log(`canceling ${index}`);
source.cancel(`canceling ${index}`);
};
}, [index]);
return (
<div>
<button
onClick={() => {
setIndex(index + 1);
}}
>
click
</button>
<div>
<img src={imgUrl} alt="" />
</div>
</div>
);
}
axios 中一个请求取消的示例 通过解读其源码不难实现出一个自己的版本。Here we go... Promise 链与拦截器这个和请求的取消其实关系不大,但不妨先来了解一下,axios 中如何组织起来一个 Promise 链(Promise chain),从而实现在请求前后可执行一个拦截器(Interceptor)的。 简单来说,通过 axios 发起的请求,可在请求前后执行一些函数,来实现特定功能,比如请求前添加一些自定义的 header,请求后进行一些数据上的统一转换等。 用法首先,通过 axios 实例配置需要执行的拦截器: axios.interceptors.request.use(function (config) {
console.log('before request')
return config;
}, function (error) {
return Promise.reject(error);
});
axios.interceptors.response.use(function (response) {
console.log('after response');
return response;
}, function (error) {
return Promise.reject(error);
});
然后每次请求前后都会打印出相应信息,拦截器生效了。 axios({
url: "https://dog.ceo/api/breeds/image/random",
method: "GET"
}).then(res => {
console.log("load success");
});
下面编写一个页面,放置一个按钮,点击后发起请求,后续示例中将一直使用该页面来测试。 import React from "react";
import axios from "axios";
export default function App() {
const sendRequest = () => {
axios.interceptors.request.use(
config => {
console.log("before request");
return config;
},
function(error) {
return Promise.reject(error);
}
);
axios.interceptors.response.use(
response => {
console.log("after response");
return response;
},
function(error) {
return Promise.reject(error);
}
);
axios({
url: "https://dog.ceo/api/breeds/image/random",
method: "GET"
}).then(res => {
console.log("load success");
});
};
return (
<div>
<button onClick={sendRequest}>click me</button>
</div>
);
}
点击按钮后运行结果:
拦截器机制的实现实现分两步走,先看请求前的拦截器。 请求前拦截器的实现Promise 的常规用法如下: new Promise(resolve,reject);
假如我们封装一个类似 axios 的请求库,可以这么写: interface Config {
url: string;
method: "GET" | "POST";
}
function request(config: Config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(config.method, config.url);
xhr.onload = () => {
resolve(xhr.responseText);
};
xhr.onerror = err => {
reject(err);
};
xhr.send();
});
}
除了像上面那个直接 Promise.resolve(value).then(()=>{ /**... */ });
这种方式创建 Promise 的好处是,我们可以从 function request(config: Config) {
return Promise.resolve(config)
.then(config => {
console.log("interceptor 1");
return config;
})
.then(config => {
console.log("interceptor 2");
return config;
})
.then(config => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(config.method, config.url);
xhr.onload = () => {
resolve(xhr.responseText);
};
xhr.onerror = err => {
reject(err);
};
xhr.send();
});
});
}
将前面示例中 axios 替换为我们自己写的
这里,已经实现了 axios 中请求前拦截器的功能。仔细观察,上面三个 于是我们可以将他们抽取成三个函数,每个函数就是一个拦截器。 function interceptor1(config: Config) {
console.log("interceptor 1");
return config;
}
function interceptor2(config: Config) {
console.log("interceptor 2");
return config;
}
function xmlHttpRequest<T>(config: Config) {
return new Promise<T>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(config.method, config.url);
xhr.onload = () => {
resolve(xhr.responseText as any);
};
xhr.onerror = err => {
reject(err);
};
xhr.send();
});
}
接下来要做的,就是从 Promise 链的头部 function request<T = any>(config: Config) {
let chain: Promise<any> = Promise.resolve(config);
chain = chain.then(interceptor1);
chain = chain.then(interceptor2);
chain = chain.then(xmlHttpRequest);
return chain as Promise<T>;
}
然后,将上面硬编码的写法程式化一下,就实现了任意个请求前拦截器的功能。 扩展配置,以接收拦截器: interface Config {
url: string;
method: "GET" | "POST";
interceptors?: Interceptor<Config>[];
}
创建一个数组,将执行请求的函数做为默认的元素放进去,然后将用户配置的拦截器压入数组前面,这样形成了一个拦截器的数组。最后再遍历这个数组形成 Promise 链。 function request<T = any>({ interceptors = [], ...config }: Config) {
// 发送请求的拦截器为默认,用户配置的拦截器压入数组前面
const tmpInterceptors: Interceptor<any>[] = [xmlHttpRequest];
interceptors.forEach(interceptor => {
tmpInterceptors.unshift(interceptor);
});
let chain: Promise<any> = Promise.resolve(config);
tmpInterceptors.forEach(interceptor => (chain = chain.then(interceptor)));
return chain as Promise<T>;
}
使用: request({
url: "https://dog.ceo/api/breeds/image/random",
method: "GET",
interceptors: [interceptor1, interceptor2]
}).then(res => {
console.log("load success");
});
执行结果:
注意这里顺序为传入的拦截器的反序,不过这不重要,可通过传递的顺序来控制。 响应后拦截器上面实现了在请求前执行一序列拦截函数,同理,如果将拦截器压入到数组后面,即执行请求那个函数的后面,便实现了响应后的拦截器。 继续扩展配置,将请求与响应的拦截器分开: interface Config {
url: string;
method: "GET" | "POST";
interceptors?: {
request: Interceptor<Config>[];
response: Interceptor<any>[];
};
}
更新 function request<T = any>({
interceptors = { request: [], response: [] },
...config
}: Config) {
const tmpInterceptors: Interceptor<any>[] = [xmlHttpRequest];
interceptors.request.forEach(interceptor => {
tmpInterceptors.unshift(interceptor);
});
interceptors.response.forEach(interceptor => {
tmpInterceptors.push(interceptor);
});
let chain: Promise<any> = Promise.resolve(config);
tmpInterceptors.forEach(interceptor => (chain = chain.then(interceptor)));
return chain as Promise<T>;
}
类似 function interceptor3<T>(res: T) {
console.log("interceptor 3");
return res;
}
function interceptor4<T>(res: T) {
console.log("interceptor 4");
return res;
}
测试代码: request({
url: "https://dog.ceo/api/breeds/image/random",
method: "GET",
interceptors: {
request: [interceptor1, interceptor2],
response: [interceptor3, interceptor4]
}
}).then(res => {
console.log("load success");
});
运行结果:
不难看出,当我们发起一次 axios 请求时,其实是发起了一次 Promise 链,链上的函数顺次执行。
因为拉弓没有回头箭,请求发出后,能够取消的是后续操作,而不是请求本身,所以上面的 Promise 链中,需要实现
请求的取消Promise 链的中断中断 Promise 链的执行,可通过 throw 异常来实现。 添加一个中间函数,将执行请求的函数进行封装,无论其成功与否,都抛出异常将后续执行中断。 function adapter(config: Config) {
return xmlHttpRequest(config).then(
res => {
throw "baddie!";
},
err => {
throw "baddie!";
}
);
}
更新 function request<T = any>({
interceptors = { request: [], response: [] },
...config
}: Config) {
- const tmpInterceptors: Interceptor<any>[] = [xmlHttpRequest];
+ const tmpInterceptors: Interceptor<any>[] = [adapter];
interceptors.request.forEach(interceptor => {
tmpInterceptors.unshift(interceptor);
});
interceptors.response.forEach(interceptor => {
tmpInterceptors.push(interceptor);
});
let chain: Promise<any> = Promise.resolve(config);
tmpInterceptors.forEach(interceptor => (chain = chain.then(interceptor)));
return chain as Promise<T>;
}
再次执行其输出结果为:
请求取消的实现按照 axios 的实现思路,要实现请求的取消,需要先创建一个 token,通过该 token 可调用一个 构造 token所以不难看出,这里的 token 对象至少:
额外地,
由此我们得到这么一个类: class CancelTokenSource {
private _canceled = false;
get canceled() {
return this._canceled;
}
private _message = "unknown reason";
get message() {
return this._message;
}
cancel(reason?: string) {
if (this.canceled) return;
if (reason) {
this._message = reason;
}
this._canceled = true;
}
}
添加 token 到配置扩展配置,以接收一个用来取消的 token 对象: interface Config {
url: string;
method: "GET" | "POST";
+ cancelToken?: CancelTokenSource;
interceptors?: {
request: Interceptor<Config>[];
response: Interceptor<any>[];
};
}
请求逻辑中处理取消同时更新 function xmlHttpRequest<T>(config: Config) {
return new Promise<T>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(config.method, config.url);
xhr.onload = () => {
resolve(xhr.responseText as any);
};
xhr.onerror = err => {
reject(err);
};
+ xhr.onabort = () => {
+ reject();
+ };
+ if (config.cancelToken) {
+ xhr.abort();
+ }
xhr.send();
});
}
取消的调用将抛异常的代码抽取成方法以在多处调用,更新 function throwIfCancelRequested(config: Config) {
if (config.cancelToken && config.cancelToken.canceled) {
throw config.cancelToken.message;
}
}
function adapter(config: Config) {
throwIfCancelRequested(config);
return xmlHttpRequest(config).then(
res => {
throwIfCancelRequested(config);
return res;
},
err => {
throwIfCancelRequested(config);
return Promise.reject(err);
}
);
}
测试请求的取消似乎一切 okay,接下来测试一波。以下代码期望每次点击按钮发起请求,请求前先取消掉之前的请求。为了区分每次不同的请求,添加 import React, { useEffect, useState } from "react";
export default function App() {
const [index, setIndex] = useState(0);
useEffect(() => {
const token = new CancelTokenSource();
request({
url: "https://dog.ceo/api/breeds/image/random",
method: "GET",
cancelToken: token,
interceptors: {
request: [interceptor1, interceptor2],
response: [interceptor3, interceptor4]
}
})
.then(res => {
console.log(`load ${index} success`);
})
.catch(err => {
console.log("outer catch ", err);
});
return () => {
token.cancel(`just cancel ${index}`);
};
}, [index]);
return (
<div>
<button
onClick={() => {
setIndex(index + 1);
}}
>
click me
</button>
</div>
);
}
加载页面进行测试, interceptor 2
interceptor 1
interceptor 3
interceptor 4
load 0 success
interceptor 2
interceptor 1
interceptor 2
interceptor 1
outer catch just cancel 1
interceptor 3
interceptor 4
load 2 success
现有实现中的问题从输出来看,
从输出和网络请求来看,有两个问题:
function throwIfCancelRequested(config: Config, flag?: number) {
if (config.cancelToken && config.cancelToken.canceled) {
console.log(flag);
throw config.cancelToken.message;
}
}
function adapter(config: Config) {
throwIfCancelRequested(config, 1);
return xmlHttpRequest(config).then(
res => {
//ℹ 后续输出证明,实际生效的是此处
throwIfCancelRequested(config, 2);
return res;
},
err => {
//ℹ 而非此处,即使取消的动作是在请求进行过程中
throwIfCancelRequested(config, 3);
return Promise.reject(err);
}
);
}
输出: interceptor 2
interceptor 1
interceptor 2
interceptor 1
2
outer catch just cancel 1
interceptor 3
interceptor 4
load 2 success
优化下面的优化需要解决上面的问题。所用到的方法便是 axios 中的逻辑,也是一开始看源码会不太理解的地方。 其实外部调用 因此,在 更新后的 class CancelTokenSource {
public promise: Promise<unknown>;
private resolvePromise!: (value?: any) => void;
constructor() {
this.promise = new Promise(resolve => {
this.resolvePromise = resolve;
});
}
private _canceled = false;
get canceled() {
return this._canceled;
}
private _message = "unknown reason";
get message() {
return this._message;
}
cancel(reason?: string) {
if (reason) {
this._message = reason;
}
this._canceled = true;
this.resolvePromise();
}
}
更新后访问 function xmlHttpRequest<T>(config: Config) {
return new Promise<T>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(config.method, config.url);
xhr.onload = () => {
resolve(xhr.responseText as any);
};
xhr.onerror = err => {
reject(err);
};
xhr.onabort = () => {
reject();
};
if (config.cancelToken) {
config.cancelToken.promise.then(() => {
xhr.abort();
});
}
xhr.send();
});
}
测试优化后的版本输出结果:
浏览器调试工具的网络会有一次飘红被 完整代码自己实现的请求取消机制完整代码import React, { useState, useEffect } from "react";
class CancelTokenSource {
public promise: Promise<unknown>;
private resolvePromise!: (value?: any) => void;
constructor() {
this.promise = new Promise(resolve => {
this.resolvePromise = resolve;
});
}
private _canceled = false;
get canceled() {
return this._canceled;
}
private _message = "unknown reason";
get message() {
return this._message;
}
cancel(reason?: string) {
if (reason) {
this._message = reason;
}
this._canceled = true;
this.resolvePromise();
}
}
type Interceptor<T> = (value: T) => T | Promise<T>;
interface Config {
url: string;
method: "GET" | "POST";
cancelToken?: CancelTokenSource;
interceptors?: {
request: Interceptor<Config>[];
response: Interceptor<any>[];
};
}
function interceptor1(config: Config) {
console.log("interceptor 1");
return config;
}
function interceptor2(config: Config) {
console.log("interceptor 2");
return config;
}
function interceptor3<T>(res: T) {
console.log("interceptor 3");
return res;
}
function interceptor4<T>(res: T) {
console.log("interceptor 4");
return res;
}
function xmlHttpRequest<T>(config: Config) {
return new Promise<T>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(config.method, config.url);
xhr.onload = () => {
resolve(xhr.responseText as any);
};
xhr.onerror = err => {
reject(err);
};
xhr.onabort = () => {
reject();
};
if (config.cancelToken) {
config.cancelToken.promise.then(() => {
xhr.abort();
});
}
xhr.send();
});
}
function throwIfCancelRequested(config: Config, flag?: number) {
if (config.cancelToken && config.cancelToken.canceled) {
console.log(flag);
throw config.cancelToken.message;
}
}
function adapter(config: Config) {
throwIfCancelRequested(config, 1);
return xmlHttpRequest(config).then(
res => {
throwIfCancelRequested(config, 2);
return res;
},
err => {
throwIfCancelRequested(config, 3);
return Promise.reject(err);
}
);
}
function request<T = any>({
interceptors = { request: [], response: [] },
...config
}: Config) {
const tmpInterceptors: Interceptor<any>[] = [adapter];
interceptors.request.forEach(interceptor => {
tmpInterceptors.unshift(interceptor);
});
interceptors.response.forEach(interceptor => {
tmpInterceptors.push(interceptor);
});
let chain: Promise<any> = Promise.resolve(config);
tmpInterceptors.forEach(interceptor => (chain = chain.then(interceptor)));
return chain as Promise<T>;
}
export default function App() {
const [index, setIndex] = useState(0);
useEffect(() => {
const token = new CancelTokenSource();
request({
url: "https://dog.ceo/api/breeds/image/random",
method: "GET",
cancelToken: token,
interceptors: {
request: [interceptor1, interceptor2],
response: [interceptor3, interceptor4]
}
})
.then(res => {
console.log(`load ${index} success`);
})
.catch(err => {
console.log("outer catch ", err);
});
return () => {
token.cancel(`just cancel ${index}`);
};
}, [index]);
return (
<div>
<button
onClick={() => {
setIndex(index + 1);
}}
>
click me
</button>
</div>
);
}
运行效果 相关资源 |
The text was updated successfully, but these errors were encountered: |