JavaScript中大批量异步任务顺序执行的三种方案与内存优化
本文探讨在浏览器端进行大体积文件分片上传时,如何处理连续异步任务的顺序执行问题。通过对比Promise链式调用、自定义EventEmitter事件模式以及基于setInterval的状态轮询三种方案,分析各自在代码可读性、调试难度及内存管理上的差异,重点解决循环异步执行导致的内存泄漏问题,为前端高性能上传组件开发提供参考。
前言
大文件分片上传的异步任务挑战
近期开发一个支持大文件上传及断点续传的浏览器组件时,遇到了一个具有挑战性的技术问题。
核心难点在于:如何在循环中执行连续的异步任务,并确保后续任务严格等待前一个任务的执行状态。
以下结合具体组件实现进行说明。
该组件旨在处理GB级别的大视频文件上传。由于无法将完整文件加载至内存,必须采用分片发送策略,单次发送大小设定为4MB,这既符合网络传输的实际状况,也契合断点续传的技术逻辑。
组件的核心工作流程如下:
- 用户选定文件后,从HTML5原生上传组件中获取文件的Blob对象(同步操作)。
- 利用Blob对象的slice方法将文件切割为多个切片(同步操作)。
- 创建FileReader实例,通过readAsArrayBuffer方法异步读取步骤2中生成的切片数据。
- 若步骤3中buffer读取成功(通过监控Filereader的onload事件),则异步发送步骤3中的buffer
- 若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(前端上传插件使用方案二,后端拼接文件切片使用方案三)。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
JS处理含Emoji或中文的字符串转数组乱码问题
2026-11-05 16:12:28
JS怎么使用展开运算符合并对象
2026-11-05 14:42:18
ES6+环境下判断JS对象为空的简洁写法
2026-11-05 12:12:23
怎么通过replacer函数自定义JS中JSON的格式化输出
2026-11-05 11:19:44
JS正则表达式基础:字面量与RegExp构造函数的区别及常用元字符
2026-11-05 11:16:58
