Welcome to follow my public Rui Talk to get my latest articles:
I. Introduction
The control of API requests has always been a hot issue in the front-end field, and there are already many excellent open source projects available on the market. In the spirit of teaching people how to fish, this article puts aside all the tool functions and introduces how to use the simplest code to solve practical problems in various scenarios.
Two, concurrency control
In some scenarios, the front-end needs to send a large number of network requests in a short period of time without occupying too many system resources, which requires concurrency control of requests. The request here may be the same interface or multiple interfaces. Generally, it is necessary to wait for all the interfaces to return before performing unified processing. In order to improve efficiency, we hope that when a request is completed, the position will be freed immediately, and then a new request will be initiated. Here we can use the integrated Promise
2 tool in the process to achieve the purpose, namely race
and all
.
async function concurrentControl(poolLimit, requestPool) {
// 存放所有请求返回的 promise
const ret = [];
// 正在执行的请求,用于控制并发
const executing = [];
while (requestPool.length > 0) {
const request = requestPool.shift();
const p = Promise.resolve().then(() => request());
ret.push(p);
// p.then()返回一个新的 promise,表示当前请求的状态
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) {
await Promise.race(executing);
}
}
return Promise.all(ret);
}
Among them, this line of code is more critical:const e = p.then(() => executing.splice(executing.indexOf(e), 1))
To understand this line of code correctly, you must understand the following features of promise
- The return value of p.then() is a
promise
, and the then function is to execute code synchronously - Role p.then () is to
p
thispromise
subscription, similar todom
ofaddEventListener
- The fn in then(fn) will not be executed asynchronously by the JS engine
promise
So the real execution order of the above code is:
const e = p.then(fn);
executing.push(e);
// p resolve 后执行 fn
() => executing.splice(executing.indexOf(e), 1)
The following is the test code, you can verify it yourself if you are interested.
let i = 0;
function generateRequest() {
const j = ++i;
return function request() {
return new Promise(resolve => {
console.log(`r${j}...`);
setTimeout(() => {
resolve(`r${j}`);
}, 1000 * j);
})
}
}
const requestPool = [generateRequest(), generateRequest(), generateRequest(), generateRequest()];
async function main() {
const results = await concurrentControl(2, requestPool);
console.log(results);
}
main();
It was used to achieve the foregoing async/await
is ES7
characteristics with ES6
can achieve the same effect.
function concurrentControl(poolLimit, requestPool) {
// 存放所有请求返回的 promise
const ret = [];
// 正在执行的请求,用于控制并发
const executing = [];
function enqueue() {
const request = requestPool.shift();
if (!request) {
return Promise.resolve();
}
const p = Promise.resolve().then(() => request());
ret.push(p);
let r = Promise.resolve();
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) {
r = Promise.race(executing);
}
return r.then(() => enqueue());
}
return enqueue().then(() => Promise.all(ret));
}
What is used here is the method of function nesting. The code is not async/await
, but it has another advantage. It supports dynamic addition of new requests:
const requestPool = [generateRequest(), generateRequest(), generateRequest(), generateRequest()];
function main() {
concurrentControl(2, requestPool).then(results => console.log(results));
// 动态添加新请求
requestPool.push(generateRequest());
}
As can be seen from the code, before the requestPool request is completed, we can dynamically add new requests to it, which is suitable for some scenarios where requests are initiated based on conditions.
Three, throttling control
The traditional throttling is to control the timing of request sending, and the throttling mentioned in this article is the design mode of publish and subscribe, the result of multiplexing the request, and it is suitable for the scenario where multiple identical requests are sent in a short period of time. code show as below:
function generateRequest() {
let ongoing = false;
const listeners = [];
return function request() {
if (!ongoing) {
ongoing = true
return new Promise(resolve => {
console.log('requesting...');
setTimeout(() => {
const result = 'success';
resolve(result);
ongoing = false;
if (listeners.length <= 0) return;
while (listeners.length > 0) {
const listener = listeners.shift();
listener && listener.resolve(result);
}
}, 1000);
})
}
return new Promise((resolve, reject) => {
listeners.push({ resolve, reject })
})
}
}
The key point here is that if there is an ongoing request, create a new promise
, store resolve
and reject
in the listeners array, and subscribe to the result of the request.
The test code is as follows:
const request = generateRequest();
request().then(data => console.log(`invoke1 ${data}`));
request().then(data => console.log(`invoke2 ${data}`));
request().then(data => console.log(`invoke3 ${data}`));
3. Cancellation request
There are two ways to realize the cancellation request, let's look at the first one first.
Set a flag to control the validity of the request, which is explained React Hooks
useEffect(() => {
// 有效性标识
let didCancel = false;
const fetchData = async () => {
const result = await getData(query);
// 更新数据前判断有效性
if (!didCancel) {
setResult(result);
}
}
fetchData();
return () => {
// query 变更时设置数据失效
didCancel = true;
}
}, [query]);
After the request is returned, the validity of the request is judged first, and if it is invalid, the subsequent operations are ignored.
The above implementation is actually not a real cancellation, it is more appropriate to say it is discarding. If you want to implement a real cancellation request, you must use the AbortController
API. The sample code is as follows:
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 5000);
fetch(url, { signal }).then(response => {
return response.text();
}).then(text => {
console.log(text);
}).catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Uh oh, an error!', err);
}
});
When calling abort()
, promise
will be rejected, triggering a AbortError
DOMException
.
4. Elimination request
In a scenario like the search box, the user needs to prompt search suggestions while typing. This requires multiple requests to be sent in a short time, and the results of the previous request cannot cover the latter (network congestion may cause the first request to be sent after the request). return). The obsolete demand can be eliminated in the following way.
// 请求序号
let seqenceId = 0;
// 上一个有效请求的序号
let lastId = 0;
function App() {
const [query, setQuery] = useState('react');
const [result, setResult] = useState();
useEffect(() => {
const fetchData = async () => {
// 发起一个请求时,序号加 1
const curId = ++seqenceId;
const result = await getData(query);
// 只展示序号比上一个有效序号大的数据
if (curId > lastId) {
setResult(result);
lastId = curId;
} else {
console.log(`discard ${result}`);
fetchData();
}, [query]);
return (
...
);
}
The key point here is whether the sequence number of the request is greater than the last valid request when the comparison request is returned. If it is not, it means that a later request was responded first, and the current request should be discarded.
Five, summary
This article lists several special scenarios when the front-end processes API requests, including concurrency control, throttling, cancellation, and elimination, and summarizes solutions based on the characteristics of each scenario, which improves performance while ensuring data validity.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。