[心得] 自己做 Promise 並行數量限制
再開始今天的主題前,因為小弟最近在看 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/Promise
縮 https://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,
12/10 18:52
推
12/10 18:53,
12/10 18:53
→
12/10 18:53,
12/10 18:53
推
12/10 18:55,
12/10 18:55
→
12/10 18:57,
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
04/25 07:55, 1F
那是個人習慣,雖然我知道 context 沒有被改變
但就怕以後加 code 會出事
→
04/25 07:55, , 2F
04/25 07:55, 2F
→
04/25 07:57, , 3F
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
04/26 01:03, 6F
Ajax 近期熱門文章
PTT數位生活區 即時熱門文章