两个实用函数:最大余额法和并发限制
概述
函数在 JavaScript 中最常见的语法之一,可以用在封装通用能力或者业务逻辑。
本文会介绍两个函数,一个是最大余额法对应的函数,另一个是异步并发限制函数。
最大余额法
最近在写饼图业务的时候,发现在取两位小数的情况下, echarts 提供的饼图百分比正好都是 100%,而如果自己计算的话,按四舍五入做很大概率总和不是100%。所有就去研究了下 echarts 的方法。
echarts 内部是使用最大余额法去计算,让我们看下它的 wiki 定义。
最大余额方法(英语:largest remainder method)又称数额制、汉米尔顿法(英语:Hamilton method),是比例代表制投票制度下,一种议席分配的方法。
我们来结合 wiki 中例子来看,假设选举投票人次100,000,分配10个议席。选举结果:
每 1 万票可以获得到 1 个议席,名单丙丁戊都获得 1 个,名单已分配 4 个,那剩下的5个怎么分配?最大余额法的分配方式是按余额大小顺序依次分配,故已(7000),乙(6100),戊(6000),丁(5800),甲(6100)各 1 个,至此分配完毕。
这和我们的计算百分比有什么关系,平时我们可能使用四舍五入处理,导致百分比总和不是百分比,现在我们可以使用最大余额法。
四舍五入:
我们假设百分比保留两位小数,那么两位小数后的就相当于余额是吧。我们先算出目前还有多少“席位”,即舍去小数点两位后的各项数值相加还差多少到 100%,如果差 0.02%,我们就把这 0.02% 按各项的余额大小来分配。
最大余额法:
这里我们可以做个优化,我们保证席位的最小单位是1,对份额和席位做个放大
接下来我们来实现它,我先整理下思路:
- 根据保留的小数位计算放大比例
- 计算每个数组项的百分比,然后在乘 100 * 比例。此时各项相加应该是 100 * 比例(乘以 100, 是因为最后返回结果为百分比)。
- 分别计算当前席位和余额情况,各项整数部分就是当前获得的席位,小数部分就是余额。
- 计算还剩多少席位未分配,根据最大余额进行分配。
function getPercentWithPrecision(valueList, precision) {
// 根据保留的小数位做对应的放大
const digits = Math.pow(10, precision)
const sum = valueList.reduce((total, cur) => total + cur, 0)
// 计算每项占比,并做放大,保证整数部分就是当前获得的席位,小数部分就是余额
const votesPerQuota = valueList.map((val) => {
return val / sum * 100 * digits
})
// 整数部分就是每项首次分配的席位
const seats = votesPerQuota.map((val) => {
return Math.floor(val);
});
// 计算各项的余额
const remainder = votesPerQuota.map((val) => {
return val - Math.floor(val)
})
// 总席位
const totalSeats = 100 * digits
// 当前已经分配出去的席位总数
let currentSeats = votesPerQuota.reduce((total, cur) => total + Math.floor(cur), 0)
// 按最大余额法分配
while(totalSeats - currentSeats > 0) {
let maxIdx = -1 // 余数最大的 id
let maxValue = Number.NEGATIVE_INFINITY // 最大余额, 初始重置为无穷小
// 选出这组余额数据中最大值
for(var i = 0; i < remainder.length; i++) {
if (maxValue < remainder[i]) {
maxValue = remainder[i]
maxIdx = i
}
}
// 对应的项席位加 1,余额清零,当前分配席位加 1
seats[maxIdx]++
remainder[maxIdx] = 0
currentSeats++
}
return seats.map((val) => `${val / totalSeats * 100}%`)
}
至此我们把最大余额法现实出来了,有兴趣的同学可以看下 echarts 里面对应方法 getPercentWithPrecision,基本大同小异。
异步并发限制
限制异步并发这个功能也很常见,如果同时发出一堆请求,网络可能就被这一堆请求占用了,其他请求就会排在后边,所以我们想限制下请求的并发数。
这边我们直接来看 async-pool-tiny 的实现
async function asyncPool(poolLimit, iterable, iteratorFn) {
// 用于保存所有异步请求
const ret = [];
// 用户保存正在进行的请求
const executing = new Set();
for (const item of iterable) {
// 构造出请求 Promise
const p = Promise.resolve().then(() => iteratorFn(item, iterable));
ret.push(p);
executing.add(p);
// 请求执行结束后从正在进行的数组中移除
const clean = () => executing.delete(p);
p.then(clean).catch(clean);
// 如果正在执行的请求数大于并发数,就使用 Promise.race 等待一个最快执行完的请求
if (executing.size >= poolLimit) {
await Promise.race(executing);
}
}
// 返回所有结果
return Promise.all(ret);
}
// 使用方法
const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i));
asyncPool(2, [1000, 5000, 3000, 2000], timeout).then(results => {
console.log(results)
})
核心实现就是使用 Promise.race 和 Promise.all。如果发出的请求大等于并发数了,这时就不会发起下一个请求,而是使用 Promise.race 等待一个最快执行结束的请求,然后在继续发请求。最后通过 Promise.all 返回所有结束。
总结
最大余额法和 asyncPool 的实现都很巧,代码优雅,很值得学习。各位有没什么有用的函数推荐一下,相互学习。
转载自:https://juejin.cn/post/7078532391832125448