Promise chain and request cancellation from Axios source code
Liu wayong 2021-06-23 19:54:47

axios An example of a request to cancel :

axios Example code to cancel the request
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 An example of a request to cancel

axios An example of a request to cancel

By reading its source code, it is not difficult to achieve a version of its own .Here we go...

Promise Chains and interceptors

This has little to do with the cancellation of the request , But let's get to know ,axios How to organize a Promise chain (Promise chain), Thus, an interceptor can be executed before and after the request (Interceptor) Of .

Simply speaking , adopt axios Requests initiated , Some functions can be executed before and after the request , To achieve specific functions , For example, add some custom header, After the request, some unified data conversion will be carried out .

usage

First , adopt axios The interceptor that the instance configuration needs to execute :

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);
});

Then the corresponding information will be printed before and after each request , The interceptor is working .

axios({
url: "https://dog.ceo/api/breeds/image/random",
method: "GET"
}).then(res => {
console.log("load success");
});

Now write a page , Place a button , Click to request , This page will always be used in subsequent examples to test .

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>
);
}

Click the button to run the result :

before request
after response
load success

Implementation of interceptor mechanism

There are two steps to realize , First look at the interceptor before the request .

Implementation of interceptor before request

Promise The general usage of is as follows :

new Promise(resolve,reject);

If we encapsulate a similar axios Request library for , It can be written like this :

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();
});
}

Except for the direct new One Promise Outside , In fact, any object value can form a Promise, Method is to call Promise.resolve,

Promise.resolve(value).then(()=>{ /**... */ });

This way to create Promise Are the benefits of , We can config Start , Create a Promise chain , Before a real request is made , Do some functions first , like this :

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();
});
});
}

In the previous example axios Replace it with something we wrote ourselves request function , The example can run normally , Output is as follows :

interceptor 1
interceptor 2
load success

here , It's done axios The function of interceptor before request in . Observe carefully , The three above then The function in , Formed a Promise chain , Execute in sequence in this chain , Each can be seen as an interceptor , Even the one who sent the request then.

So we can extract them into three functions , Every function is a Interceptor .

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();
});
}

What to do next , It's from Promise The head of the chain Promise.resolve(config) Start , String the three functions above . With the help of Monkey patch It's not hard to achieve :

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>;
}

then , Program the above hard coding method , It realizes the function of interceptor before any request .

Extended configuration , To receive interceptors :

interface Config {
url: string;
method: "GET" | "POST";
interceptors?: Interceptor<Config>[];
}

Create an array , Put the function that executes the request as the default element , Then press the user configured interceptor in front of the array , This forms an array of interceptors . Finally, traverse the array to form Promise chain .

function request<T = any>({ interceptors = [], ...config }: Config) {
// The interceptor that sends the request is the default , User configured interceptors are pushed in front of the array 
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>;
}

Use :

request({
url: "https://dog.ceo/api/breeds/image/random",
method: "GET",
interceptors: [interceptor1, interceptor2]
}).then(res => {
console.log("load success");
});

Execution results :

interceptor 2
interceptor 1
load success

Note that the order here is the reverse of the incoming interceptor , But it doesn't matter , It can be controlled by the order of transmission .

Post response interceptors

The above implementation performs a sequence of intercepting functions before the request , Empathy , If you push the interceptor behind the array , That is, after the function to execute the request , The response interceptor is implemented .

Continue to expand configuration , Separate the request from the interceptor of the response :

interface Config {
url: string;
method: "GET" | "POST";
interceptors?: {
request: Interceptor<Config>[];
response: Interceptor<any>[];
};
}

to update request Method , The logic of the interceptor before the request remains the same , Pass the new response interceptor through push Press into the back of the array :

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>;
}

similar interceptor1 interceptor2, Two new interceptors are added for post response execution ,

function interceptor3<T>(res: T) {
console.log("interceptor 3");
return res;
}
function interceptor4<T>(res: T) {
console.log("interceptor 4");
return res;
}

Test code :

request({
url: "https://dog.ceo/api/breeds/image/random",
method: "GET",
interceptors: {
request: [interceptor1, interceptor2],
response: [interceptor3, interceptor4]
}
}).then(res => {
console.log("load success");
});

Running results :

interceptor 2
interceptor 1
interceptor 3
interceptor 4
load success

It's not hard to see. , When we launch a axios When asked , In fact, it was launched once Promise chain , The functions on the chain execute in sequence .

request interceptor 1
request interceptor 2
...
request
response interceptor 1
response interceptor 2
...

Because the bow did not turn back , After the request , What can be cancelled is the subsequent operation , Not the request itself , So the top Promise In the chain , Need to achieve request Subsequent interceptors and subsequent callbacks cancel execution .

request interceptor 1
request interceptor 2
...
request
# Subsequent operations are no longer performed
response interceptor 1
response interceptor 2
...

Requested cancellation

Promise Chain break

interrupt Promise Chain execution , It can be done by throw Exception to achieve .

Add an intermediate function , Encapsulate the function that executes the request , Whether it succeeds or not , Throw an exception to interrupt subsequent execution .

function adapter(config: Config) {
return xmlHttpRequest(config).then(
res => {
throw "baddie!";
},
err => {
throw "baddie!";
}
);
}

to update request Function USES adapter Instead of using it directly xmlHttpRequest

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>;
}

The output of executing it again is :

interceptor 2
interceptor 1
Uncaught (in promise) baddie!

Implementation of request cancellation

according to axios How to realize , To achieve the requested cancellation , I need to create one first token, Through the token You can call a cancel Method ; By way of token Pass to configuration , When making a request token Check to determine the token Has cancellation been performed , If so, use the above ideas , take Promise The chain is broken .

structure token

So it's not hard to see , there token Object at least :

  • There is one cancel Method
  • There is a field record cancel Whether the method has been called

Extra ,

  • If there is a field record cancellation reason , That's not bad .

So we get a class :

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;
}
}

add to token To configuration

Extended configuration , To receive a cancellation token object :

interface Config {
url: string;
method: "GET" | "POST";
+ cancelToken?: CancelTokenSource;
interceptors?: {
request: Interceptor<Config>[];
response: Interceptor<any>[];
};
}

Processing cancellation in request logic

Simultaneous updating xmlHttpRequest function , Judge token Whether the state of has been called to cancel , If so, call xhr.abort(), Simultaneous addition onabort Callback to reject fall Promise:

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();
});
}

Cancelled call

Extract the exception throwing code into methods to call in multiple places , to update adapter The logic of , Normal return and... Without cancellation reject.

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);
}
);
}

Cancellation of test request

It seems that everything okay, Next test a wave of . The following code expects each click of a button to initiate a request , Cancel the previous request before requesting . To distinguish between different requests , add to index Variable , When the button is clicked, it will increase automatically .

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>
);
}

Load the page for testing ,useEffect It will run for the first time after the page is loaded , Will trigger a complete request process . Then click the button twice in a row , To cancel the last of the two . Running results :

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

Problems in existing implementations

In terms of output ,

  • The first part is the first request , It's a normal request .
  • The second part is the execution of the request interceptor for the first click .
  • The third part is the second click , First request cancelled , Then complete a complete request .

In terms of output and network requests , There are two questions :

  • xhr.abort() No entry into force , In two successive clicks , There will be two statuses in the browser debugging tool 200 Request .
  • The first request subsequent callbacks are actually cancelled , But it's waiting for the request to succeed , Cancelled in a successful callback , This can be seen by adding flag bits to the cancel function .
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 => {
// Subsequent output proof , In effect here 
throwIfCancelRequested(config, 2);
return res;
},
err => {
// Not here , Even if the cancelled action is in the process of request 
throwIfCancelRequested(config, 3);
return Promise.reject(err);
}
);
}

Output :

interceptor 2
interceptor 1
interceptor 2
interceptor 1
2
outer catch just cancel 1
interceptor 3
interceptor 4
load 2 success

Optimize

The following optimization needs to solve the above problems . The method used is axios Logic in , It's also a place where you don't understand the source code at first .

In fact, external calls cancel() The timing is uncertain , therefore token The field on the object that records whether it has been cancelled , When is it set to true It's uncertain , therefore , We cancel the logic of the request (xhr.abort()) It should be in a Promise Middle to finish .

therefore , stay CancelTokenSource Class , Create a Promise Type field , It will be cancel() When the method is called resolve fall .

Updated CancelTokenSource class :

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();
}
}

Visit... After update canceled The logic of the field :

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();
});
}

Test the optimized version

Output results :

interceptor 2
interceptor 1
interceptor 3
interceptor 4
load 0 success
interceptor 2
interceptor 1
interceptor 2
3
interceptor 1
outer catch just cancel 1
interceptor 3
interceptor 4
load 2 success

The network of browser debugging tools will be red once abort The request to drop , At the same time, the output above ( Where it works is 3 Instead of 2) Show cancelled requests correctly reject It fell off .

Complete code

The complete code of the request cancellation mechanism implemented by myself
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>
);
}

 Running effect

Running effect

Related resources

The text was updated successfully, but these errors were encountered:

Please bring the original link to reprint ,thank
Similar articles