并发控制
对于短时间可能发送大量网络请求的场景,为了节约资源,需要进行请求的并发控制。设置最大并发数,当某个请求完成时,才发起新的请求:
/**
* 请求并发控制
* @param {*} requestPool 请求池 (是一个可迭代对象)
* @param {*} poolLimit 最大并发数
*/
/** es7 */
async function concurrencyControl(requestPool, poolLimit) {
/** 用于请求的结果 */
const ret = [];
/** 真正并发执行的请求集合 */
const executing = new Set();
for (const item of requestPool) {
/** 防止返回的不是promise,使用Promise.resolve */
const p = Promise.resolve().then(() => item());
/** 将正在请求的promise放入ret和executing中 */
ret.push(p);
executing.add(p);
/** 请求resolve 或 reject 执行 清除操作 */
const clean = () => executing.delete(p);
p.then(clean).catch(clean);
if (executing.size >= poolLimit) {
// 一旦正在执行的promise列表数量等于限制数,就使用Promise.race等待某一个promise状态发生变更,
// 状态变更后,就会执行上面then的回调,将该promise从executing中删除,
// 然后再进入到下一次for循环,生成新的promise进行补充
await Promise.race(excuting);
}
}
return Promise.all(ret);
}
/** es6 */
function concurrencyControlES6(requestPool, poolLimit) {
let i = 0;
const ret = [];
const executing = new Set();
const enqueue = function() {
if (i === requestPool.length) {
return Promise.resolve();
}
const item = iterable[i++];
const p = Promise.resolve().then(() => item());
ret.push(p);
executing.add(p);
const clean = () => executing.delete(p);
p.then(clean).catch(clean);
let r = Promise.resolve();
if (executing.size >= poolLimit) {
r = Promise.race(executing);
}
return r.then(() => enqueue());
};
return enqueue().then(() => Promise.all(ret));
}
async/await
是 ES7
的特性,用 ES6
也能实现相同的效果,而且ES6的方式的可以动态的添加新的请求:
function main() {
concurrentControlES6(requestPool, 2).then(res => console.log(res));
/** 动态添加新请求 */
requestPool.push(newRequest());
}
取消请求
想实现真正的取消请求,就要用到 AbortController
API:
const abortController = new AbortController();
const signal = abortController.signal;
setTimeout(() => abortController.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);
}
});
当调用 abort()
时,promise
会被 reject 掉,触发一个名为 AbortError
的 DOMException
。
节流控制
通过发布订阅的设计模式,对请求的结果进行复用,适用于在短时间内发送多个相同请求的场景。
关键在于如果有正在进行的请求,则新建一个 promise
,将 resolve
和 reject
存到 listeners 数组中,订阅请求的结果。:
function reqThrottler(delay) {
/** 是否正在请求中 */
let isReqing = false;
/** 订阅请求 resolve 和 reject 的数组*/
const listeners = [];
return (query, request) => {
/** 正在请求中,新建一个promise存到listerers中 */
if (isReqing) {
return new Promise((resolve, reject) => {
listeners.push({ resolve, reject });
})
}
isReqing = true;
return new Promise(resolve => {
/** 正在请求中... */
const p = Promise.resovle().then(() => request(query));
let res;
p.then((value) => {
res = value;
return resolve(value);
});
setTimeout(() => {
isReqing = false;
if (listeners.length <= 0) return;
while (listeners.length > 0) {
const listener = listeners.shift();
listener && listener.resolve(res);
}
}, delay);
})
}
}
淘汰请求
根据搜索词展示关联词的场景,短时间会发送大量的请求,这时我们要保证先发起的请求如果后返回是需要淘汰掉的。我们可以通过比较请求返回时,请求的序号是不是比上一个有效请求大。如果不是,则说明一个后面发起的请求先响应了,当前的请求应该丢弃:
const reqDisuser = () => {
/**上一个有效请求的序号 */
let preId = 0;
/** 请求序号 */
let seqenceId = 0;
return (query, requestFn) => new Promise((resovle, reject) => {
const resolveHandler = (value) => {
/** 成功返回,只取比上一个有效请求大的结果 */
if(seqenceId > preId) {
preId = seqenceId;
resolve(value);
} else {
reject(`被淘汰的请求, 请求序号=${seqenceId}`)
};
}
const rejectHandler = (err) => {
reject(err);
}
const p = Promise.resolve().then(() => {
/** 发起一个请求时,序号加 1 */
seqenceId = seqenceId + 1;
return requestFn(query);
});
p.then(resolveHandler).catch(rejectHandler);
})
}
我们就可以判断 requestFn 返回结果是否正确,来决定是否展示,因为淘汰的请求被reject掉了。