作者s25g5d4 (function(){})()
看板Ajax
标题[心得] 自己做 Promise 并行数量限制
时间Mon Apr 25 00:52:43 2016
再开始今天的主题前,因为小弟最近在看 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 怎麽写
--
1F:→ eggimage:我之前也发生过很多次 yahoo本来就很烂 还外加奇摩12/10 18:52
2F:推 madeinchina:之前即时通死都不让我登入 後来我就改用MSN了...12/10 18:53
3F:→ eggimage:发现MSN也不给你登....12/10 18:53
4F:推 madeinchina: 就改登PTT了12/10 18:55
5F:→ eggimage:最近ptt也一直断....12/10 18:57
6F:→ freely10469: 只好掀桌出去裸奔了...12/10 19:57
--
※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 140.117.181.25
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/Ajax/M.1461516776.A.5CE.html
※ 编辑: s25g5d4 (140.117.181.25), 04/25/2016 01:00:50
7F:→ dlikeayu: 箭头函式那边应该不用事前 let _self = this 04/25 07:55
那是个人习惯,虽然我知道 context 没有被改变
但就怕以後加 code 会出事
8F:→ dlikeayu: 用箭头函式时已经等同宣告里面的this是外面那层的了 04/25 07:55
9F:→ dlikeayu: 另外你专案再用webpack的loader来处理一下,供给大家你 04/25 07:57
10F:→ dlikeayu: 喜好的语法方式就比较没有相容性问题了 04/25 07:57
我只有用 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;`
11F:→ dlikeayu: 感谢你的分享 04/25 07:57
※ 编辑: s25g5d4 (140.117.247.129), 04/25/2016 10:25:36
※ 编辑: s25g5d4 (140.117.247.129), 04/25/2016 10:29:57
12F:→ No: 你这写法 [].map(q.queue) 会喷掉喔 04/26 01:03