本文最后更新于:2020年12月30日 凌晨
为什么需要它
有些时候不得不需要限制并发 fetch 的请求数量,避免请求过快导致 IP 封禁
需要做到什么
- 允许限制 fetch 请求同时存在的数量
- 时间过久便认为是超时了
如何实现
暂停请求
该方法的请求是无序的!
- 使用 class 定义默认超时设置和请求数量限制的构造函数
- 在请求前判断当前请求的数量,添加请求等待数量
- 如果请求数量已满,则进行等待
- 如果请求数量未满,则删除一个请求等待数量
- 请求完成,删除当前请求数量
等待队列:循环监听
该方法需要使用回调函数
- 使用 class 定义默认超时设置和请求数量限制的构造函数
- 在请求前将请求 argments 添加到等待队列中
- 使用
setInterval
函数持续监听队列和当前执行的请求数
- 发现请求数量没有到达最大值,且等待队列中还有值,那么就执行一次请求
等待队列:触发钩子
- 使用 class 定义默认超时设置和请求数量限制的构造函数
- 在请求前将请求 argments 添加到等待队列中
- 添加完成,等待当前请求数量未满
- 尝试启动等待队列(钩子)
实现代码
暂停请求实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
|
function wait(param) { return new Promise(resolve => { if (typeof param === "number") { setTimeout(resolve, param); } else if (typeof param === "function") { var timer = setInterval(() => { if (param()) { clearInterval(timer); resolve(); } }, 100); } else { resolve(); } }); }
function promiseTimeout(fetchPromise, timeout) { var abortFn = null; var abortPromise = new Promise(function(resolve, reject) { abortFn = function() { reject("abort promise"); }; }); var abortablePromise = Promise.race([fetchPromise, abortPromise]); setTimeout(function() { abortFn(); }, timeout);
return abortablePromise; }
class RequestLimiting { constructor({ timeout = 10000, limit = 10 }) { this.timeout = timeout; this.limit = limit; this.execCount = 0; this.waitCount = 0; }
async _fetch(url, init) { this.waitCount++; await wait(() => this.execCount < this.limit); this.waitCount--; this.execCount++; try { return await promiseTimeout(fetch(url, init), this.timeout); } finally { this.execCount--; } } }
|
使用示例
1 2 3 4 5 6 7
| const requestLimiting = new RequestLimiting({ timeout: 500, limit: 1 }); new Array(100).fill(0).forEach(i => requestLimiting ._fetch("/") .then(res => console.log(res)) .catch(err => console.log(err)) );
|
等待队列:循环监听实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
|
function wait(param) { return new Promise(resolve => { if (typeof param === "number") { setTimeout(resolve, param); } else if (typeof param === "function") { var timer = setInterval(() => { if (param()) { clearInterval(timer); resolve(); } }, 100); } else { resolve(); } }); }
function promiseTimeout(fetchPromise, timeout) { var abortFn = null;
var abortPromise = new Promise(function(resolve, reject) { abortFn = function() { reject("abort promise"); }; });
var abortablePromise = Promise.race([fetchPromise, abortPromise]);
setTimeout(function() { abortFn(); }, timeout);
return abortablePromise; }
class RequestLimiting { constructor({ timeout = 10000, limit = 10 }) { this.timeout = timeout; this.limit = limit; this.execCount = 0; this.waitArr = [];
setInterval(async () => { if (this.execCount >= this.limit) { return; } console.debug( `执行 execCount: ${this.execCount}, waitArr length: ${ this.waitArr.length }, index: ${JSON.stringify(this.waitArr[0])}` ); const args = this.waitArr.shift(0); if (!args) { return; } this.execCount++; const callback = args[2]; try { callback({ res: await promiseTimeout(fetch(...args), this.timeout) }); } catch (err) { callback({ err: err }); } finally { this.execCount--; } }, 100); }
async _fetch(url, init, callback) { this.waitArr.push(arguments); } }
|
使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const requestLimiting = new RequestLimiting({ timeout: 500, limit: 1 }); new Array(100).fill(0).forEach((item, i) => requestLimiting._fetch( "/", { headers: { index: i } }, ({ res, err }) => { console.log(`res: ${res}, err: ${err}`); } ) );
|
等待队列:触发钩子实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
|
function wait(param) { return new Promise(resolve => { if (typeof param === "number") { setTimeout(resolve, param); } else if (typeof param === "function") { var timer = setInterval(() => { if (param()) { clearInterval(timer); resolve(); } }, 100); } else { resolve(); } }); }
function promiseTimeout(fetchPromise, timeout) { var abortFn = null; var abortPromise = new Promise(function(resolve, reject) { abortFn = function() { reject("abort promise"); }; }); var abortablePromise = Promise.race([fetchPromise, abortPromise]); setTimeout(function() { abortFn(); }, timeout); return abortablePromise; }
class RequestLimiting { constructor({ timeout = 10000, limit = 10 }) { this.timeout = timeout; this.limit = limit; this.execCount = 0; this.waitArr = []; }
async _fetch(url, init) { const _innerFetch = async () => { console.log( `执行 execCount: ${this.execCount}, waitArr length: ${ this.waitArr.length }, index: ${JSON.stringify(this.waitArr[0])}` ); this.execCount++; const args = this.waitArr.shift(0); try { return await promiseTimeout(fetch(...args), this.timeout); } finally { this.execCount--; } }; this.waitArr.push(arguments); await wait(() => this.execCount < this.limit); return _innerFetch(); } }
|
使用示例
1 2 3 4 5 6 7 8 9 10 11 12
| const requestLimiting = new RequestLimiting({ timeout: 500, limit: 1 }); new Array(100).fill(0).forEach((item, i) => requestLimiting ._fetch("/", { headers: { index: i } }) .then(res => console.log(res)) .catch(err => console.log(err)) );
|
总结
目前而言,最后一种实现是最好的,同时实现了两种规范
- 返回
Promise
,避免使用回调函数
- 请求执行与添加顺序相同