原生fetch 实现并发控制
前端时间面试过程中遇到一道手写题
题目:如何使用原生fetch 实现一个并发控制
要求:
1.通过传入参数进行控制fetch的最大并发量
2. 不影响fetch的正常使用,也就是说在代码中可以将你使用的fetch替换成你写好的函数即可,其他逻辑不动
3. 实现fetch超时设置
举例:
var url = 'http://192.168.6.41:19010/api/h5/query_repay_order';
var _fetch = handlerFetch(10, 1000);
new Array(50).fill(1).forEach((i, index) => {
_fetch(url);
});
首先看下这个举例,我们要实现一个handlerFetch函数,该函数接受两个参数,第一个参数控制最大并发量,第二个参数是控制fetch的timeout,时间是毫秒。
handlerFetch函数应该返回的是一promise,这样就可以保证_fetch函数也可以进行链式调用。
再看这个例子,我们可以看到下面调用_fetch是连续调用,控制并发量是10,超时控制是1000ms,根据这些参数,整理一下思路。
思路:
-
创建一个handlerFetch函数(参数有两个一个为并发控制,一个为超时设置)
-
获取_fetch调用次数count(此处可以考虑使用闭包进行计数)
-
比较count和最大并发控制limit 小于limit的请求可以直接执行 大于limit的请求可以推入一个队列
-
等待前面fetch请求完成后再执行队列中等待的请求
-
超时设置,可以在handlerFetch中统一设置,或是在每一个请求中设置,每一个请求中设置超时优先级高于统一设置(超时可以考虑使用abortController,这个兼容性虽然不好,但是可以真正取消请求)
那我们按照思路一步步进行实现吧。
第一步:实现一个handlerFetch函数
/**
* 创建一个handlerFetch
*
* @param {limit,timeout} limit 为并发控制 timeout为超时设定
* @return function 返回一个函数
*/
function handlerFetch (limit, timeout) {
limit = limit || 1;
timout = timeout || 0;
var count = 0, pool = [];
return function (url) {
// 其他处理
}
}
第二步:获取调用_fetch次数
/**
* 创建一个handlerFetch
*
* @param {limit,timeout} limit 为并发控制 timeout为超时设定
* @return function 返回一个函数
*/
function handlerFetch (limit, timeout) {
limit = limit || 1;
timout = timeout || 0;
var count = 0, pool = [];
return function (url) {
count++;
console.log(count);
}
}
// 测试
var _fetch = handlerFetch(10, 1000);
_fetch(); // 1
_fetch(); // 2
_fetch(); // 3
第三步:比较count和最大并发控制limit,小于于limit的直接执行,大于limit的推入pool任务队列
/**
* 创建一个handlerFetch
*
* @param {limit,timeout} limit 为并发控制 timeout为超时设定
* @return function 返回一个函数
*/
function handlerFetch (limit, timeout) {
limit = limit || 1;
timout = timeout || 0;
var count = 0, pool = [];
return function (url) {
// 每一个fetch请求都是一个task
var task = () => new Promise((resolve, reject) => {
fetch(url)
.then(res => {
resolve(res);
})
.catch(err => {
reject(err);
})
})
};
// 比较count与limit 大于等于limit的推入等待队列 小于limit的 count + 1,并执行fetch请求
if (count >= limit) {
pool.push(task);
} else {
++count;
task();
}
}
第四步:等待前面fetch请求完成后再执行队列中等待的请求
/**
* 创建一个handlerFetch
*
* @param {limit,timeout} limit 为并发控制 timeout为超时设定
* @return function 返回一个函数
*/
function handlerFetch (limit, timeout) {
limit = limit || 1;
timout = timeout || 0;
var count = 0, pool = [];
return function (url) {
// 每一个fetch请求都是一个task
// 当fetch请求成功或失败的时候 通过next 执行下一个等待队列中的请求
var task = () => new Promise((resolve, reject) => {
fetch(url)
.then(res => {
resolve(res);
next();
})
.catch(err => {
reject(err);
next();
})
})
};
// 定一个next 控制等待队列中的请求继续并发调用
var next = () => {
// 每执行一次next count - 1,然后比较当前的count 与 limit
// 如果小于limit 循环执行limit-count 次
count--;
if (count < limit && pool.length) {
var n = limit - count;
for (var i = 0; i < n; i++) {
var curTask = pool.shift();
curTask();
++count;
}
}
}
// 比较count与limit 大于等于limit的推入等待队列 小于limit的 count + 1,并执行fetch请求
if (count >= limit) {
pool.push(task);
} else {
++count;
task();
}
}
第五部:添加超时设置
/**
* 创建一个handlerFetch
*
* @param {limit,timeout} limit 为并发控制 timeout为超时设定
* @return function 返回一个函数
*/
function handlerFetch (limit, timeout) {
limit = limit || 1;
timeout = timeout || 0;
var count = 0, pool = [];
return function (url, options) {
// 通过AbortController 控制 取消fetch 请求
var controller = new AbortController();
var signal = controller.signal;
// 判断是否需要超时
var isTimeout = options && options.timeout || timeout
// 控制请求超时
var timeoutPromise = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('请求超时');
controller.abort();
}, options && options.timeout !== undefined ? options.timeout : timeout)
})
}
// 返回fetch 本身
var taskPromise = () => new Promise((resolve, reject) => {
fetch(url, { signal, ...options }).then(res => {
resolve(res);
}).catch(err => {
reject(err)
})
});
// 通过Promise.race可以控制超时,并在访问结果中 去继续调用等待池中的请求
var task = () => (isTimeout ? Promise.race([timeoutPromise(), taskPromise()]) : taskPromise())
.then((res) => {
console.log('res', res);
next();
})
.catch(err => {
next();
console.log('err', err);
});
// 定一个next 控制等待队列中的请求继续并发调用
var next = () => {
// 每执行一次next count - 1,然后比较当前的count 与 limit
// 如果小于limit 循环执行limit-count 次
count--;
if (count < limit && pool.length) {
var n = limit - count;
for (var i = 0; i < n; i++) {
var curTask = pool.shift();
curTask();
++count;
}
}
};
// 比较count与limit 大于等于limit的推入等待队列 小于limit的 count + 1,并执行fetch请求
if (count >= limit) {
pool.push(task);
} else {
++count;
task();
}
}
}
测试效果1:limit = 1, timeout = 1000
var url = 'http://192.168.6.41:19010/api/h5/query_repay_order'
var _fetch = handlerFetch(1, 1000);
new Array(50).fill(1).forEach((i, index) => {
_fetch(url)
})
试效果2:limit = 5, timeout = 1000
var url = 'http://192.168.6.41:19010/api/h5/query_repay_order'
var _fetch = handlerFetch(5, 1000);
new Array(50).fill(1).forEach((i, index) => {
_fetch(url)
})
var url = 'http://192.168.6.41:19010/api/h5/query_repay_order'
var _fetch = handlerFetch(1);
new Array(50).fill(1).forEach((i, index) => {
_fetch(url, {timeout: (index + 1) * 200})
})