JavaScript中大批量异步任务顺序执行的三种方案与内存优化

作者:拾光时间:2026-10-22 20:10:50浏览:214

本文探讨在浏览器端进行大体积文件分片上传时,如何处理连续异步任务的顺序执行问题。通过对比Promise链式调用、自定义EventEmitter事件模式以及基于setInterval的状态轮询三种方案,分析各自在代码可读性、调试难度及内存管理上的差异,重点解决循环异步执行导致的内存泄漏问题,为前端高性能上传组件开发提供参考。

前言

大文件分片上传的异步任务挑战

近期开发一个支持大文件上传及断点续传的浏览器组件时,遇到了一个具有挑战性的技术问题。

核心难点在于:如何在循环中执行连续的异步任务,并确保后续任务严格等待前一个任务的执行状态。

以下结合具体组件实现进行说明。

该组件旨在处理GB级别的大视频文件上传。由于无法将完整文件加载至内存,必须采用分片发送策略,单次发送大小设定为4MB,这既符合网络传输的实际状况,也契合断点续传的技术逻辑。

组件的核心工作流程如下:

  1. 用户选定文件后,从HTML5原生上传组件中获取文件的Blob对象(同步操作)。
  2. 利用Blob对象的slice方法将文件切割为多个切片(同步操作)。
  3. 创建FileReader实例,通过readAsArrayBuffer方法异步读取步骤2中生成的切片数据。
  4. 若步骤3中buffer读取成功(通过监控Filereader的onload事件),则异步发送步骤3中的buffer
  5. 若Ajax发送成功且服务器存储完成,服务器会返回特定的成功状态码。客户端检测到response中存在该状态码后,触发下一次切片发送(异步操作)。

从上述流程可见,步骤3、4、5中的连续异步任务必须严格按顺序执行,且各步骤间存在紧密的数据依赖,最终还需对这一系列步骤进行多次循环处理。

若仅处理单次连续异步任务,使用Promise链式调用即可解决;但涉及循环执行此类连续异步任务时,实现逻辑较为复杂。

经过大量资料检索未果后,经过两天的独立研究,提出了三种解决方案。旨在抛砖引玉,期待社区提供更优建议。

这三种方案的核心思想均类似于观察者模式,旨在控制循环流程,主要区别在于循环的具体实现方式。这实际上也是作者自我否定、不断优化的过程。由于组件代码较长,此处仅展示与问题相关的核心部分,并省略了错误处理逻辑。

方案一:Promise链式调用的内存陷阱

方案一

仍以上传组件为例进行说明。


//循环状态标记,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());
}

方案一基于对问题的直观理解,即遇到异步任务便使用Promise,通过长链调用实现循环。这种写法不够优雅,错误调试极为困难。更严重的是,由于闭包问题,循环执行过程中内存难以回收,导致内存消耗急剧增加,只能等待整个循环执行完毕才能释放。

方案二:基于EventEmitter的事件驱动模式

方案二

彻底引入观察者模式,构建一个简单的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')
 }
})

方案二完全贯彻“事件驱动”理念,代码语义更加自然,错误调试也比方案一简单得多。然而,内存泄漏问题依然存在。

方案三:利用setInterval解决内存泄漏

方案三

方案三回归方案一的状态管理思路,但改用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)

不可否认,方案三看起来较为简陋。若追求极致的执行效率,这或许是最笨的办法。但方案三实际上将异步任务转化为了同步执行的逻辑,语义简洁,且避免了前两种方法的内存泄漏问题。

方案三的本质是将while(true)改写为setInterval。因为while(true)会阻塞主线程,导致各种异步事件的回调也被阻塞,因此选择了setInterval作为替代方案。

总结与社区求助

总结

当时还尝试过使用Object.defineProperty给status绑定setter方法,通过每次设置新值时判断循环条件,但发现这依然类似链式调用,同样存在内存泄漏问题,故此处省略。

坦白说,这三个方案均存在较大缺陷,甚至可以说较为浅显。本人前端经验仅两个月,视野有限,在Google无果后,向社区求助,希望各位前辈提供更佳的思路。

最后附上文中提到的上传插件代码。因感觉仍有缺陷,未进行封装,仅制作了Demo(前端上传插件使用方案二,后端拼接文件切片使用方案三)。

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

作者最新文章