最近須要作一個瀏覽器的, 支持大致積文件上傳且要支持斷點續傳的上傳組件, 原本覺得很容易的事情, 結果碰到了一個有意思的問題:前端
循環執行連續的異步任務, 且後一個任務須要等待前一個任務的執行狀態node
這麼說可能有點空泛, 以我作的組件舉例:git
這個組件本意是爲了上傳大致積視頻, 和支持斷點續傳, 由於動輒幾個G的視頻不可能直接把文件讀進內存, 只能分片發送(考慮到實際網絡狀態, 每次發送大小定在了4MB), 並且這麼作也符合斷點續傳的思路.github
組件工做流程以下:ajax
從組件工做流程能夠發現, 3,4,5中的連續異步任務, 必需要按順序進行, 且每一步任務間存在相互依賴, 最後還要對這些步驟進行屢次循環.後端
若是隻是處理單次的連續異步任務, 經過promise鏈式調用便可, 可是要循環執行這樣的連續異步任務讓我想了好久.promise
後來google了好久也沒發現解決方案, 無奈下閉門造車了2天, 想出了3套方案, 權當拋磚引玉, 但願各位給出更好建議瀏覽器
3套方案的核心思想相同, 相似觀察者模式, 來控制循環的進行, 區別在於循環的實現不一樣, 實際上這3套方案也是我自我否認的過程, 不斷思考更好的方法, 整個組件代碼略長, 在此只挑出問題相關部分, 且省略錯誤處理部分服務器
依然以上傳組件舉例網絡
//循環狀態標記,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, 這樣的循環長鏈調用, 寫法不優雅, 且錯誤調試異常麻煩, 更爆炸的是由於閉包問題, 在循環執行中這些內存難以回收, 內存消耗急劇增長, 只能等待循環執行完成
完全引入觀察者模式, 構造一個簡單的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, 迴歸方案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)