前言
在大文件上传实现专栏的前三篇文章,带大家逐步实现了大文件上传的切片上传、断点续传、秒传等前后端功能。如果对大文件上传的原理不了解,可以先看看前面三篇文章。
需求分析
前面的文章中,例子中用的大文件大小是1.5G, 而切片大小固定是100M(实际的大小,可能还得根据网络带宽、服务器性能等其他因素考虑,与服务器协商确定,文章就不展开了),那么切片数量就有16个了,而我们注意到,同一个域名下面,最多并发6个请求,多余的得排队了。如下图,虽然是用了Promise.all()
来同时进行16个请求,但实际是最多六个。
那么问题来了,如果页面有其他要发送请求的业务,此时会被排到最后了,这样子不好。那么就需要进行并发控制了,可以控制大文件上传并发的数量,不至于完全占用网络资源,阻碍其他业务。
异步并发控制
异步并发控制,套到大文件上传场景,具体到这个项目,有15个文件切片上传,期待的是能够控制同时上传的请求数量,比如三个或者4个。
完成这种控制功能的库,我了解到的有 async-pool
怎么用呢?输入是三个参数,第一个是并发量,第二个是待处理的数据数组,第三个是处理数据的异步函数。
参考该库的源码,来实现一下:
js
async function asyncPool(limit, arr, handleFn) {
// 用来装所有的promise
const ret = [];
// 用来装正在执行的promise
const executing = new Set();
// 循环arr, 用 Promise.race 来执行
for (let item of arr) {
const p = Promise.resolve()
.then(() => handleFn(item))
.finally(() => executing.delete(p)); // 执行完后删除
// 放到队列中
ret.push(p);
executing.add(p);
// 当正在执行的promise数量达到limit时,进行并发控制,等待一个promise完成再继续循环
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(ret);
}
注意是用了ES7的语法,才看起来思路这么清晰。
测试代码,处理函数是延迟一秒resolve
,用来并发控制,那么期待的结果是:每隔一秒打印两个数据,直到打印完所有数据。
js
const handleFn = (data) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("data: ", data);
resolve(data);
}, 1000);
});
};
const results = await asyncPool(2, [1, 2, 3, 4, 5, 6, 7], handleFn);
console.log("results: ", results);
看看效果:
没问题,那么整合到的上传文件流程中吧。
先把原本循环发送请求的部分代码改一下,包成函数,而不是立即请求:
diff
- const requests = chunks.map((chunkInfo) => {
+ const requestFns = chunks.map((chunkInfo) => {
+ const requestFn = () => {
const cancelToken = axios.CancelToken.source();
cancelTokens.push(cancelToken);
// 查找是否有上传过的切片
const uploadedChunk = uploadedChunks.find(
(item) => item.chunkFilename === chunkInfo.chunkFilename
);
...
+ };
+ return requestFn;
});
使用 asyncPool 函数来控制
diff
- await Promise.all(requests);
+ await asyncPool(3, requestFns, (requestFn) => requestFn());
来看看效果:
🥳 如愿,效果是每次三个请求,那么就达成了我们期待的效果了,优化完成。
补充
上面asyncPool的实现,用的ES7的语法,来实现一版兼容性好一点的,采用了异步递归的方式:
js
function asyncPool(limit, arr, handleFn) {
// 用来装所有的promise
const ret = [];
// 用来装正在执行的promise
const executing = new Set();
let i = 0;
const loop = () => {
// 递归终止条件
if (i === arr.length) {
return Promise.resolve();
}
const p = Promise.resolve()
.then(() => handleFn(arr[i++]))
.finally(() => executing.delete(p)); // 执行完后删除
ret.push(p);
executing.add(p);
// 当正在执行的promise数量达到limit时,进行并发控制,等待一个promise完成再继续循环
let r = Promise.resolve();
if (executing.size >= limit) {
r = Promise.race(executing);
}
return r.then(loop);
};
return loop().then(() => Promise.all(ret));
}
测试方式一样,这种实现绕一点,但实现效果是一样的。
总结
这篇文章接着文件上传实现的最终功能,提出了一个问题,多个切片并发上传,会把网络资源全部占用了,如果有其他请求,那么得排队,这不是期待的效果,期待的效果是能够控制上传的并发数量,给其他业务保留一点网络请求资源。然后参考了 asyncPool 的实现,写了一版并整合到上传的代码中,实现了大文件上传的并发控制功能。可以把并发数量交给用户设置,也可以设定一个合适的默认值(<6),如果超过6,那跟没做优化的效果一样,大可不必。
代码放到gitee,下一篇文章,我们来看看相反的操作功能实现-大文件下载,敬请期待。