淺談關于JS下大批量異步任務按順序執(zhí)行解決方案一點思考
前言
最近需要做一個瀏覽器的, 支持大體積文件上傳且要支持斷點續(xù)傳的上傳組件, 本來以為很容易的事情, 結果碰到了一個有意思的問題:
循環(huán)執(zhí)行連續(xù)的異步任務, 且后一個任務需要等待前一個任務的執(zhí)行狀態(tài)
這么說可能有點空泛, 以我做的組件舉例:
這個組件本意是為了上傳大體積視頻, 和支持斷點續(xù)傳, 因為動輒幾個G的視頻不可能直接把文件讀進內存, 只能分片發(fā)送(考慮到實際網(wǎng)絡狀態(tài), 每次發(fā)送大小定在了4MB), 而且這么做也符合斷點續(xù)傳的思路.
組件工作流程如下:
- 選定上傳文件后, 從H5原生upload組件里取得文件的blob對象 (同步)
- 通過blob對象的slice方法把文件切片 (同步)
- 新建一個Filereader對象, 通過Filereader的readAsArrayBuffer方法讀取步驟2中生成的slice (異步)
- 如果步驟3的buffer讀取成功(通過監(jiān)控Filereader的onload事件), 則ajax發(fā)送步驟3中的buffer (異步)
- 如果ajax發(fā)送成功, 且服務器儲存完成, 會向客戶端發(fā)回一個成功狀態(tài)碼, 如果ajax的response中存在這個狀態(tài)碼, 則進行下一次切片發(fā)送 (異步)
從組件工作流程可以發(fā)現(xiàn), 3,4,5中的連續(xù)異步任務, 必須要按順序進行, 且每一步任務間存在相互依賴, 最后還要對這些步驟進行多次循環(huán).
如果只是處理單次的連續(xù)異步任務, 通過promise鏈式調用即可, 但是要循環(huán)執(zhí)行這樣的連續(xù)異步任務讓我想了很久.
后來google了很久也沒發(fā)現(xiàn)解決方案, 無奈下閉門造車了2天, 想出了3套方案, 權當拋磚引玉, 希望各位給出更好建議
3套方案的核心思想相同, 類似觀察者模式, 來控制循環(huán)的進行, 區(qū)別在于循環(huán)的實現(xiàn)不同, 實際上這3套方案也是我自我否定的過程, 不斷思考更好的方法, 整個組件代碼略長, 在此只挑出問題相關部分, 且省略錯誤處理部分
方案1
依然以上傳組件舉例
//循環(huán)狀態(tài)標記,0為初始狀態(tài),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發(fā)送createReader方法讀取到的Buff
const createXhr = ()=> {
const xhr= new XMLHttpRequest();
return new Promise ((reslove, reject)=> {
...
xhr.onreadystatechange= ()=> {
...
//如果readyState == 4,status == 200且服務器的狀態(tài)碼存在,更改全局標記為1
status = 1;
reslove()
}
})
}
//每一輪循環(huán)開始前都檢查一次全局狀態(tài)標記
const checkStatus = ()=> {
...
if (status == 1) {
loop()
}
}
//循環(huán)過程的鏈式調用
const loop = ()=> {
createReader().then(()=> createXhr()).then(()=> checkStatus());
}
方案1是基于初見問題的'想當然'解決方法, 碰到異步任務就promise, 這樣的循環(huán)長鏈調用, 寫法不優(yōu)雅, 且錯誤調試異常麻煩, 更爆炸的是因為閉包問題, 在循環(huán)執(zhí)行中這些內存難以回收, 內存消耗急劇增加, 只能等待循環(huán)執(zhí)行完成
方案2
徹底引入觀察者模式, 構造一個簡單的EventEmitter, 通過event.on, event.emit的形式完成循環(huán)
//模仿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();
...
//監(jiān)聽createReader事件,如果讀取buffer成功就觸發(fā)toajax事件來上傳切片
ev.on('createReader', ()=> {
let reader = new Filereader();
...
reader.onload = ()=> {
ev.emit('toajax')
}
})
//監(jiān)聽toajax事件,如果上傳成功,就觸發(fā)createReader事件開始讀取下一切片
ev.on('toajax', ()=> {
let xhr= new XMLHttpRequest();
...
xhr.onreadystatechange = ()=> {
//如果readyState == 4,status == 200且服務器的狀態(tài)碼存在
ev.emit('createReader')
}
})
方案2徹底貫徹'事件', 代碼語義更自然, 錯誤調試也比方案1更為簡單, 但內存泄漏問題依然存在
方案3
方案3, 回歸方案1的狀態(tài)管理方式, 但是通過setInterval方法來實現(xiàn)循環(huán).
//全局狀態(tài)標記
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且服務器的狀態(tài)碼存在
status = 2
}
}
/* 設置一個間隔時間極短的計時器,根據(jù)status決定下一步的任務,
* 上傳完成后定時器自動清除自己
* 另外有判斷文件是否上傳完成的方法,這里就不寫了
*/
let timer = setInterval(()=> {
if (status == 2) {
createReader();
} else if (status == 1) {
createXhr();
} else if (status == 3) {
clearInterval(timer);
}
},10)
不可否認, 方案3看上去很low, 如果追求極致的執(zhí)行效率, 方案3無疑是最蠢的辦法, 但是方案三相當于把異步任務轉化為了同步任務, 語義簡潔, 且沒有上面2種方法的內存泄漏問題.
方案3本質上是把while (true)改寫成了setInterval, 因為while true會阻塞線程, 各種異步事件的回調也會被一同阻塞, 所以選擇了setInterval
總結
當時還嘗試過使用Object.defineProperty方法給status 綁一個set方法, 通過每次給status set新值的時候來判斷循環(huán), 但是發(fā)現(xiàn)這樣做依然像是鏈式調用, 一樣存在內存泄漏問題, 這里就不寫了.
說實話, 這3個方案感覺都有很大缺陷, 甚至可以說粗淺, 本人入坑前端2個月, 眼界有限無可避免, google無門后, 想到社區(qū)來求助, 希望老哥們提供更好的思路.
最后掛上文中提到的上傳插件, 因為感覺還有缺陷就沒封裝, 只做了個demo(前端上傳插件用的方案2, 后端拼接文件切片用的方案3)
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。

