[心得] 自己做 Promise 並行數量限制

看板Ajax作者 (function(){})()時間8年前 (2016/04/25 00:52), 8年前編輯推噓0(006)
留言6則, 2人參與, 最新討論串1/1
再開始今天的主題前,因為小弟最近在看 ES6 所以以下會用到不少 ES6 才有的 syntax 例如 let http://es6.ruanyifeng.com/#docs/let class http://es6.ruanyifeng.com/#docs/class Promise http://es6.ruanyifeng.com/#docs/promise arrow function http://es6.ruanyifeng.com/#docs/function 如果有閱讀困難歡迎在推文留言,小弟會盡量回答 -- 我們先來回顧一下 Promise https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promisehttps://goo.gl/jIfcbN Promise 有幾個靜態方法: .all(), race() 其中 .all() 返回一個 Promise 物件 當參數中所有 Promise 物件都解決 (resolve) 後才跟著解決 因此所有參數 Promise 物件會同時併發,沒有限制數量 如果是做 fetch(), XMLHttpRequest() 等,可能會因為發起太多連線而被伺服器封鎖 但是原生的 Promise 物件並沒有提供限制並行數量的功能 所以我們有兩種選擇 1. 自幹 2. 找 lib 稍微翻了一下, async.js 其實就有提供這功能 但是因為我寫的是 user script 雖然可以用建置工具(如 Browserify)解決 import 問題 不過我還是希望盡量不靠額外的模組(code size 有差,雖然其實是不是問題的問題) 另外一個問題是我要做的是全域的並行數量限制 所以可能會在不同時間加入新的非同步請求 而不是一次執行所有非同步請求,再限制同時執行數量 所以其實我要做的其實是 Queue (隊列) 就像排隊買東西一樣,只有一個隊列 然後有 n 個櫃台可以同時服務使用者 所以我們可以訂下一個 API: class Queue { // 建立 Queue 物件,並行數量限制 limit 個 constructor(limit){} // 將 asyncFunction 放進 queue 中 // asyncFunction 將收到兩個參數 resolve, reject // 與 Promise() 的參數相同 // 回傳一個 Promise 物件 queue(asyncFunction){} // 內部方法:當數量充足時執行非同步請求 dequeue(){} } 接著先從 constructor() 著手 首先當然要把限制數量存起來 this.limit = limit; 接著建立一個陣列存放傳進來的非同步請求 this.q = []; 以及已經執行,等待解決的請求 this.slot = []; queue() 的部分回傳的是一個 Promise 物件 而 Promise 的接受的參數將會立即執行 所以這裡不能直接執行 asyncFunction 應該把 asyncFunction 連同 resolve(), reject() 放入 this.q 中 let _self = this; let job = new Promise( (resolve, reject) => { _self.q.push({ 'run': asyncFunction, 'resolver': resolve, 'rejector': reject }); }); 接著立即執行 dequeue(),若 slot 還有空間就執行請求 這裡把判斷限制數量放在之後的 dequeue 中 _self.dequeue(); 回傳剛剛建立的 Promise 物件 return job; 接著著手 dequeue(),第一件事當然是判斷 slot 有沒有空間 再把之前放進 q 的請求拿出來執行 let _self = this; if (_self.slot.length < _self.limit && _self.q.lenth >= 1) { let job = _self.q.shift(); _self.slot.push(job); job.run( (data) => { _self.removeJob(job); // will implement later setTimeout(_self.dequeue.bind(_self), 0); // will run after current function ends job.resolver(data); job = null; // always release memory }, (reason) => { _self.removeJob(job); setTimeout(_self.dequeue.bind(_self), 0); job.rejector(reason); job = null; }); } 這裡就變得比較複雜了,對吧? 還記得 Promise 物件的參數 executor 接受的參數 resolve 與 reject 嗎 我們先傳入自訂的 resolve() 與 reject() 進 asyncFunction 待 asyncFunction 結束請求而返回時,先將額外的事情處理完 才真正執行剛剛一起 push 進 q 的 resolve() 與 reject() 而下一行的 setTimeout 作用在於推遲下一個 dequeue 動作 先把目前處理中的請求結束掉,才能跑下一個請求 所以透過 setTimeout(dequeue, 0) 將其推遲至當前函數執行完才接著執行 接著再執行真正的 resolve 與 reject 讓剛剛 queue() 回傳的 Promise 有結果 而上述第一次出現的 removeJob() 則只有一個功能:移除 job(廢話) 程式碼只有短短幾行: Queue.prototype.removeJob = (job) => { let index = this.slot.indexOf(job); if (index >= 0) this.slot.splice(index, 1); }; 這樣我們就做完一個可以限制並行數量的非同步請求 Queue 了 \⊙▽⊙/ -- 那做完了要怎麼用呢? 首先要建立 Queue 物件: let q = new Queue(3); // max parallel limit: 3 接著放進 executor 並得到一個 Promise 物件 q.queue( (resolve, reject) => { XMLHttpRequest({ .... 'onload': resolve, 'onerror': reject }); }) .then( (data) => console.log(data.responseText)) .catch( (reason) => console.log(reason)); 或是透過 Promise.all 一次發出多個請求 Promise.all([asyncFunc1, asyncFunc2, ...].map(q.queue)) .then( (data) => console.log(data.responseText)) .catch( (reason) => console.log(reason)); -- 最後附上完整的 code https://gist.github.com/s25g5d4/c1bfa0569b9ef7e66d1aaa7a0b75e4dd/3e2347b0eec83771293de932a8bb7369f3a00b08 縮:https://goo.gl/Gc2euE 此程式已在 Firefox Developer Edtion 47.0a2 測試 如果要在不支援 ES6 的瀏覽器上使用,可以使用 Babel.js 轉換 另外這是我目前開發用的 code https://gist.github.com/s25g5d4/c1bfa0569b9ef7e66d1aaa7a0b75e4dd 主要差別在於 timeout 設計(很重要)與 job name (debug 用) 為什麼 timeout 很重要?因為全部的 slot 可能都被占用永遠不 resolve... 然後可能會再寫一篇解釋 timeout 怎麼寫 --

12/10 18:52,
我之前也發生過很多次 yahoo本來就很爛 還外加奇摩
12/10 18:52

12/10 18:53,
之前即時通死都不讓我登入 後來我就改用MSN了...
12/10 18:53

12/10 18:53,
發現MSN也不給你登....
12/10 18:53

12/10 18:55,
就改登PTT了
12/10 18:55

12/10 18:57,
最近ptt也一直斷....
12/10 18:57

12/10 19:57,
只好掀桌出去裸奔了...
12/10 19:57
-- ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 140.117.181.25 ※ 文章網址: https://www.ptt.cc/bbs/Ajax/M.1461516776.A.5CE.html ※ 編輯: s25g5d4 (140.117.181.25), 04/25/2016 01:00:50

04/25 07:55, , 1F
箭頭函式那邊應該不用事前 let _self = this
04/25 07:55, 1F
那是個人習慣,雖然我知道 context 沒有被改變 但就怕以後加 code 會出事

04/25 07:55, , 2F
用箭頭函式時已經等同宣告裏面的this是外面那層的了
04/25 07:55, 2F

04/25 07:57, , 3F
另外你專案再用webpack的loader來處理一下,供給大家你
04/25 07:57, 3F

04/25 07:57, , 4F
喜好的語法方式就比較沒有相容性問題了
04/25 07:57, 4F
我只有用 browserify 我是把 build task 寫在 npm 裡面: "scripts": { "build": "browserify index.js -o xxx.user.js -t [ babelify \ --presets [ es2015 stage-3 ] ] -p [ browserify-header --raw \ --file header.js ]" } 其實我是認為用 ES6 export 就可以了 雖然我沒用過 webpack 不過透過 babel 應該能正確處理 `export default Queue;` 不然就透過 `module.exportsx = Queue;`

04/25 07:57, , 5F
感謝你的分享
04/25 07:57, 5F
※ 編輯: s25g5d4 (140.117.247.129), 04/25/2016 10:25:36 ※ 編輯: s25g5d4 (140.117.247.129), 04/25/2016 10:29:57

04/26 01:03, , 6F
你這寫法 [].map(q.queue) 會噴掉喔
04/26 01:03, 6F
文章代碼(AID): #1N7FdeNE (Ajax)
文章代碼(AID): #1N7FdeNE (Ajax)