likes
comments
collection
share

两个实用函数:最大余额法和并发限制

作者站长头像
站长
· 阅读数 15

概述

函数在 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,对份额和席位做个放大

两个实用函数:最大余额法和并发限制

接下来我们来实现它,我先整理下思路:

  1. 根据保留的小数位计算放大比例
  2. 计算每个数组项的百分比,然后在乘 100 * 比例。此时各项相加应该是 100 * 比例(乘以 100, 是因为最后返回结果为百分比)。
  3. 分别计算当前席位和余额情况,各项整数部分就是当前获得的席位,小数部分就是余额。
  4. 计算还剩多少席位未分配,根据最大余额进行分配。
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 的实现都很巧,代码优雅,很值得学习。各位有没什么有用的函数推荐一下,相互学习。