浅谈关于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)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。