浅谈关于JS下大批量异步任务按顺序执行解决方案一点思考
前言
最近需要做一个浏览器的,支持大体积文件上传且要支持断点续传的上传组件,本来以为很容易的事情,结果碰到了一个有意思的问题:
循环执行连续的异步任务,且后一个任务需要等待前一个任务的执行状态
这么说可能有点空泛,以我做的组件举例:
这个组件本意是为了上传大体积视频,和支持断点续传,因为动辄几个G的视频不可能直接把文件读进内存,只能分片发送(考虑到实际网络状态,每次发送大小定在了4MB),而且这么做也符合断点续传的思路.
组件工作流程如下:
- 选定上传文件后,从H5原生upload组件里取得文件的blob对象 (同步)
- 通过blob对象的slice方法把文件切片 (同步)
- 新建一个Filereader对象,通过Filereader的readAsArrayBuffer方法读取步骤2中生成的slice (异步)
- 如果步骤3的buffer读取成功(通过监控Filereader的onload事件),则ajax发送步骤3中的buffer (异步)
- 如果ajax发送成功,且服务器储存完成,会向客户端发回一个成功状态码,如果ajax的response中存在这个状态码,则进行下一次切片发送 (异步)
从组件工作流程可以发现,3,4,5中的连续异步任务,必须要按顺序进行,且每一步任务间存在相互依赖,最后还要对这些步骤进行多次循环.
如果只是处理单次的连续异步任务,通过promise链式调用即可,但是要循环执行这样的连续异步任务让我想了很久.
后来google了很久也没发现解决方案,无奈下闭门造车了2天,想出了3套方案,权当抛砖引玉,希望各位给出更好建议
3套方案的核心思想相同,类似观察者模式,来控制循环的进行,区别在于循环的实现不同,实际上这3套方案也是我自我否定的过程,不断思考更好的方法,整个组件代码略长,在此只挑出问题相关部分,且省略错误处理部分
方案1
依然以上传组件举例
//循环状态标记,0为初始状态,1为正常,2为出错
letstatus=0;
/*新建Filereader,读取文件切片,返回一个promise
*把读取成功的arraybuffer通过reslove传出
*/
constcreateReader=()=>{
returnnewPromise((reslove,reject)=>{
letreader=newFilereader();
...
reader.onload=()=>{
reslove(reader.result)
}
reader.onerror=()=>reject()
})
}
//ajax发送createReader方法读取到的Buff
constcreateXhr=()=>{
constxhr=newXMLHttpRequest();
returnnewPromise((reslove,reject)=>{
...
xhr.onreadystatechange=()=>{
...
//如果readyState==4,status==200且服务器的状态码存在,更改全局标记为1
status=1;
reslove()
}
})
}
//每一轮循环开始前都检查一次全局状态标记
constcheckStatus=()=>{
...
if(status==1){
loop()
}
}
//循环过程的链式调用
constloop=()=>{
createReader().then(()=>createXhr()).then(()=>checkStatus());
}
方案1是基于初见问题的'想当然'解决方法,碰到异步任务就promise,这样的循环长链调用,写法不优雅,且错误调试异常麻烦,更爆炸的是因为闭包问题,在循环执行中这些内存难以回收,内存消耗急剧增加,只能等待循环执行完成
方案2
彻底引入观察者模式,构造一个简单的EventEmitter,通过event.on,event.emit的形式完成循环
//模仿node.js的EventEmitter
classEventEmitter{
constructor(){
this.handler={};
}
on(eventName,callback){
if(!this.handles){
this.handles={};
}
if(!this.handles[eventName]){
this.handles[eventName]=[];
}
this.handles[eventName].push(callback);
}
emit(eventName,...arg){
if(this.handles[eventName]){
for(vari=0;i{
letreader=newFilereader();
...
reader.onload=()=>{
ev.emit('toajax')
}
})
//监听toajax事件,如果上传成功,就触发createReader事件开始读取下一切片
ev.on('toajax',()=>{
letxhr=newXMLHttpRequest();
...
xhr.onreadystatechange=()=>{
//如果readyState==4,status==200且服务器的状态码存在
ev.emit('createReader')
}
})
方案2彻底贯彻'事件',代码语义更自然,错误调试也比方案1更为简单,但内存泄漏问题依然存在
方案3
方案3,回归方案1的状态管理方式,但是通过setInterval方法来实现循环.
//全局状态标记
letstatus=0;
//读取切片
constcreateReader=()=>{
letreader=newFilereader();
...
reader.onload=()=>status=1
}
//上传切片
constcreateXhr=()=>{
letxhr=newXMLHttpRequest();
...
xhr.onreadystatechange=()=>{
...
//如果readyState==4,status==200且服务器的状态码存在
status=2
}
}
/*设置一个间隔时间极短的计时器,根据status决定下一步的任务,
*上传完成后定时器自动清除自己
*另外有判断文件是否上传完成的方法,这里就不写了
*/
lettimer=setInterval(()=>{
if(status==2){
createReader();
}elseif(status==1){
createXhr();
}elseif(status==3){
clearInterval(timer);
}
},10)
不可否认,方案3看上去很low,如果追求极致的执行效率,方案3无疑是最蠢的办法,但是方案三相当于把异步任务转化为了同步任务,语义简洁,且没有上面2种方法的内存泄漏问题.
方案3本质上是把while(true)改写成了setInterval,因为whiletrue会阻塞线程,各种异步事件的回调也会被一同阻塞,所以选择了setInterval
总结
当时还尝试过使用Object.defineProperty方法给status绑一个set方法,通过每次给statusset新值的时候来判断循环,但是发现这样做依然像是链式调用,一样存在内存泄漏问题,这里就不写了.
说实话,这3个方案感觉都有很大缺陷,甚至可以说粗浅,本人入坑前端2个月,眼界有限无可避免,google无门后,想到社区来求助,希望老哥们提供更好的思路.
最后挂上文中提到的上传插件,因为感觉还有缺陷就没封装,只做了个demo(前端上传插件用的方案2,后端拼接文件切片用的方案3)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。