事情是这样,上周四给团队专栏发了一篇文章,发完之后忘了把账号切回自己的账号。然后下午搬砖中场休息的时候刷掘金,在首页看到一篇关于限制并发请求的文章,觉得有意思就点开看了。我扫了一眼文章中的题目,没有看文章内容,然后就开始自己解题了。尴尬的是我审错题了,一开始用团队账号在评论里写了个 naive 的答案。然而更尴尬的是在我意识到自己审错题之后,还写不出正确答案……
题目是这样的:
忽略我一开始那个 naive 的答案,在我准备正确地解这个题的时候,想的是写一个通用方法,限制异步请求能执行的次数。那个通用函数写出来了,长这样:
const limitConcurrency = (fn, max) => {
const pendingTasks = new Set();
return async function(...args) {
while (pendingTasks.size >= max) {
await Promise.race(pendingTasks);
}
const promise = fn.apply(this, args);
const res = promise.catch(() => {});
pendingTasks.add(res);
await res;
pendingTasks.delete(res);
return promise;
};
};
复制代码
这个 limitConcurrency
函数在闭包里记录了当前还没 resolve 的 promise,然后在 while
循环里判断当前进行中的异步操作是否达到了设定的上限;如果达到了就用 Promise.race
等最快的那个异步执行完;当并发的异步操作数量没有达到上限时,继续执行当前的异步操作,并将当前的异步操作加到 pendingTasks 里面,在当前异步操作 resolve 的时候再将其从 pendingTask 里面删掉。
然后再回到题目,一开始我思维比较局限,想着我必须要等最后一个异步请求执行完再执行回调。怎么判断所有请求都执行完了呢?我又用了一个 naive 的方法,当 urls 数组里面最后一个请求执行完了,我就当所有请求执行完了。我最后这样写的:
function sendRequest(urls, max, callback) {
const limitFetch = limitConcurrency(fetch, max);
async function go(urlList){
const [head, ...tail] = urlList;
if(tail.length === 0) {
await limitFetch(head);
return callback();
}
limitFetch(head);
go(tail);
}
go(urls);
}
复制代码
动脑子想一想也知道最后一个请求不一定是最后执行完。但是我卡在这里了,没办法了,然后我发沸点求助了,然后各路英雄各显神通,看到他们的答案我感到怀疑人生,这么简单的问题我怎么就卡壳了?
精彩答案有很多,这里我只挑出我觉得最精彩的答案,来自幻☆精灵:
function sendRequest(urls, max, callback) {
const len = urls.length;
let idx = 0;
let counter = 0;
function _request() {
// 有请求,有通道
while (idx < len && max > 0) {
max--; // 占用通道
fetch(urls[idx++]).finally(() => {
max++; // 释放通道
counter++;
if (counter === len) {
return callback();
} else {
_request();
}
});
}
}
_request();
}
复制代码
这个答案如此精巧简洁,我今天上午看到这个答案,然后出去爬山,整个途中都在回味这段代码的精妙…… 它用最少的信息表达了异步和并发的本质。写出这种代码,需要的不仅仅是编程技巧,更多的是对异步和并发的理解。在我完整理解了这段代码之后,晚上回到家再来看我之前的答案,发现我离正确答案就只差一丢丢了!
最终完整答案如下:
const limitConcurrency = (fn, max) => {
const pendingTasks = new Set();
return async function(...args) {
while (pendingTasks.size >= max) {
await Promise.race(pendingTasks);
}
const promise = fn.apply(this, args);
const res = promise.catch(() => {});
pendingTasks.add(res);
await res;
pendingTasks.delete(res);
return promise;
};
};
async function sendRequest(urls, max, callback) {
const limitFetch = limitConcurrency(fetch, max);
await Promise.all(urls.map(limitFetch));
callback();
}
复制代码
limitConcurrency
已经保证了我的全部请求的并发上限,我只需要用 Promise.all
来处理每个并发频道的最后请求就行了……
从代码简洁性来看,当然是幻☆精灵前辈的解法更优,但我觉得我的解法也不无可取之处。我提供的答案最大的好处是,它抽象出了一个通用函数,这个通用函数能复用。
作者:serialcoder
链接:https://juejin.im/post/5c8e3401f265da67da2b0acf
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。