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