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: |
從 axios 原始碼中瞭解到的 Promise 鏈與請求的取消
相關文章
- axios原始碼分析——取消請求iOS原始碼
- axios CancelToken 取消頻繁傳送請求的用法和原始碼解析iOS原始碼
- Axios 原始碼解讀 —— 網路請求篇iOS原始碼
- axios原始碼分析——請求流程iOS原始碼
- axios取消請求 CancelToken(如何使用)iOS
- vue axios路由跳轉取消所有請求VueiOS路由
- 使用js閉包實現可取消的axios請求JSiOS
- vue中axios請求的封裝VueiOS封裝
- Vue路由切換 & Axios介面取消重複請求Vue路由iOS
- axios請求超時,設定重新請求的完美解決方法iOS
- axios 請求iOS
- 如何實現一個HTTP請求庫——axios原始碼閱讀與分析HTTPiOS原始碼
- vue中axios請求資料VueiOS
- axios的post請求爬坑iOS
- 前端進階(2)使用fetch/axios時, 如何取消http請求前端iOSHTTP
- 從原始碼分析Axios原始碼iOS
- axios請求超時解決方案iOS
- 非同步請求與中斷 ( XHR,Axios,Fetch對比 )非同步iOS
- koa原始碼中的promise原始碼Promise
- 【axios】XHR的ajax封裝+axios攔截器呼叫+請求取消iOS封裝
- Vue 使用 Axios 傳送請求的請求體問題VueiOS
- AFNetworking(一)從一次請求瞭解AFHTTPSessionManagerHTTPSession
- 談談axios中Post請求變成OPTIONS的幾種解決方案iOS
- 從原始碼裡面瞭解vue的nextTick的使用原始碼Vue
- 封裝axios請求封裝iOS
- Promise 原始碼:then 鏈式呼叫Promise原始碼
- axios躺坑之路:cookie,簡單請求與非簡單請求。iOSCookie
- Axios 原始碼解讀iOS原始碼
- 取消Fetch API請求API
- [JavaScript] Promise 與 Ajax/AxiosJavaScriptPromiseiOS
- 網路請求優化之取消請求優化
- 非同步請求xhr、ajax、axios與fetch的區別比較非同步iOS
- 一比一還原axios原始碼(二)—— 請求響應處理iOS原始碼
- SpringMVC原始碼分析:POST請求中的檔案處理SpringMVC原始碼
- 封裝ajax、axios請求封裝iOS
- vue axios 請求跨域VueiOS跨域
- axios中POST請求變成OPTIONS處理iOS
- vue 中promise 非同步請求資料VuePromise非同步