浅谈关于JS下大批量异步任务按顺序执行解决方案一点思考


Posted in Javascript onJanuary 08, 2019

前言

最近需要做一个浏览器的, 支持大体积文件上传且要支持断点续传的上传组件, 本来以为很容易的事情, 结果碰到了一个有意思的问题:

循环执行连续的异步任务, 且后一个任务需要等待前一个任务的执行状态

这么说可能有点空泛, 以我做的组件举例:

这个组件本意是为了上传大体积视频, 和支持断点续传, 因为动辄几个G的视频不可能直接把文件读进内存, 只能分片发送(考虑到实际网络状态, 每次发送大小定在了4MB), 而且这么做也符合断点续传的思路.

组件工作流程如下:

  1. 选定上传文件后, 从H5原生upload组件里取得文件的blob对象  (同步)
  2. 通过blob对象的slice方法把文件切片  (同步)
  3. 新建一个Filereader对象, 通过Filereader的readAsArrayBuffer方法读取步骤2中生成的slice  (异步)
  4. 如果步骤3的buffer读取成功(通过监控Filereader的onload事件), 则ajax发送步骤3中的buffer  (异步)
  5. 如果ajax发送成功, 且服务器储存完成, 会向客户端发回一个成功状态码, 如果ajax的response中存在这个状态码, 则进行下一次切片发送  (异步)

从组件工作流程可以发现, 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)

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Firefox window.close()的使用注意事项
Apr 11 Javascript
js的参数有长度限制吗?发现不能超过2083个字符
Apr 20 Javascript
jQuery+HTML5实现图片上传前预览效果
Aug 20 Javascript
如何利用JS通过身份证号获取当事人的生日、年龄、性别
Jan 22 Javascript
原生js封装二级城市下拉列表的实现代码
Jun 16 Javascript
JavaScript读二进制文件并用ajax传输二进制流的方法
Jul 18 Javascript
AngularJS基础 ng-focus 指令简单示例
Aug 01 Javascript
vue.js入门教程之绑定class和style样式
Sep 02 Javascript
详解Angular的内置过滤器和自定义过滤器【推荐】
Dec 26 Javascript
Vue 实时监听窗口变化 windowresize的两种方法
Nov 06 Javascript
vue实现密码显示与隐藏按钮的自定义组件功能
Apr 23 Javascript
原生JS封装拖动验证滑块的实现代码示例
Jun 01 Javascript
vue-cli2 构建速度优化的实现方法
Jan 08 #Javascript
一个因@click.stop引发的bug的解决
Jan 08 #Javascript
JavaScript学习笔记之图片库案例分析
Jan 08 #Javascript
JavaScript学习笔记之DOM操作实例分析
Jan 08 #Javascript
vue单文件组件lint error自动fix与styleLint报错自动fix详解
Jan 08 #Javascript
说说如何在Vue.js中实现数字输入组件的方法
Jan 08 #Javascript
小试SVG之新手小白入门教程
Jan 08 #Javascript
You might like
destoon调用企业会员公司形象图片的实现方法
2014/08/21 PHP
php的闭包(Closure)匿名函数详解
2015/02/22 PHP
thinkphp 中的volist标签在ajax操作中的特殊性(推荐)
2018/01/15 PHP
JavaScript自定义数组排序方法
2015/02/12 Javascript
浅谈JS中的bind方法与函数柯里化
2016/08/10 Javascript
Nodejs 发送Post请求功能(发短信验证码例子)
2017/02/09 NodeJs
vue组件中点击按钮后修改输入框的状态实例代码
2017/04/14 Javascript
打造通用的匀速运动框架(实例讲解)
2017/10/17 Javascript
iview tabs 顶部导航栏和模块切换栏的示例代码
2019/03/04 Javascript
详解vue 不同环境配置不同的打包命令
2019/04/07 Javascript
[04:40]2016国际邀请赛中国区预选赛全程TOP10镜头集锦
2016/07/01 DOTA
跟老齐学Python之用while来循环
2014/10/02 Python
利用Python命令行传递实例化对象的方法
2016/11/02 Python
opencv python统计及绘制直方图的方法
2019/01/21 Python
python实现飞机大战小游戏
2019/11/08 Python
Python:二维列表下标互换方式(矩阵转置)
2019/12/02 Python
Python使用os.listdir和os.walk获取文件路径
2020/05/21 Python
win10下python3.8的PIL库安装过程
2020/06/08 Python
Python DataFrame使用drop_duplicates()函数去重(保留重复值,取重复值)
2020/07/20 Python
使用html5新特性轻松监听任何App自带返回键的示例
2018/03/13 HTML / CSS
中国综合性网上购物商城:当当(网上卖书起家)
2016/11/16 全球购物
Capitol Lighting的1800lighting.com:住宅和商业照明
2019/04/10 全球购物
三星印度官网:Samsung印度
2019/08/03 全球购物
Vision Direct比利时:在线订购隐形眼镜
2019/08/27 全球购物
产品促销活动策划书
2014/01/15 职场文书
出国导师推荐信
2014/01/16 职场文书
干部现实表现材料
2014/02/13 职场文书
委托书范文
2014/04/02 职场文书
公开承诺书格式
2014/05/21 职场文书
抗洪救灾先进集体事迹材料
2014/05/26 职场文书
监察建议书
2015/02/04 职场文书
宝宝满月祝酒词
2015/08/10 职场文书
合作协议书格式范本
2016/03/21 职场文书
2019企业给员工的慰问信
2019/06/24 职场文书
2019如何书写演讲稿?
2019/07/01 职场文书
java固定大小队列的几种实现方式详解
2021/07/15 Java/Android