前端怎样实现一个轮询

14,698 阅读4分钟

背景

首先作为前端开发大家肯定遇到过业务系统中需要实时获取某个数据的状态或者实时推送某个状态到后端。一般有几种解决方案:一种是使用websocket,可以让后端主动推送数据到前端;还有一种是前端主动轮询(可能还细分为长轮询和短轮询,这里统一叫轮询吧)。而实际开发中为了多快好省的完成开发任务可能会更多的选择轮询来实现服务端实时方案。

轮询遇到的坑

在古早的时候遇到一种情况就是后端需要前端主动轮询然后返回一个时间戳,把这个时间戳再携带发送回后端,当时实现之后运行一段时间后总会出问题导致时间戳计算错误,最后排查是网络原因导致上一个请求未处理完下一个请求先处理造成携带的时间戳不是预期的数据,现在分析其原因应该是:

  1. 前端实现使用的是setInterval,原理来讲setInterval很适合间隔一段时间执行一次动作,但轮询又比较特殊,轮询的操作是一个异步动作(ajax请求),所以受限制网络或者服务器处理速度的原因不能保证在一定的时间间隔内前一个请求能返回,然后请求下一个请求。

  2. 后来知道了一个概念叫【电梯效应】(也可能理解的不对)大致的意思就是一个长期运行的电梯总有出错的时候,而与使用setInterval这种总是预期能正常运行的情况是相违背的;还有一种解释是日常我们做电梯会发现电梯通常不是先发的先到达,也类似于公交车先发的不一定先到。并且会出现同时到达的情况(对于服务来说这就是并发吧)

解决方案

针对上面分析得出的结论需要对应的解决方案

  1. 首先放弃setIntervalapi使用setTimeout来进行延时设置,setTimeout相对于setInterval更好控制延时,并且再上一个请求返回后才开始下一个定时,这样就可以控制同一个客户端轮询是按顺序发出的,如有跟顺序强关联的业务也不会出问题

  2. 应对第二个问题则需要动态的设置延迟时间比如以120秒为基准,每次的延时时间再加一个动态值120 + 40秒 或者 120 - 40秒的范围内,这样就可以最大限度的避免请求并发到达服务器的情况

  3. 同时在实现方法上可以充分利用新的语言特性Generator或者async/await来同步写出轮询代码,这里更倾向于async/await实现,更易读

实现代码

上面分析是可以通过setTimeout + async/await + 动态延时时间,当然这里还需要注意一点就是轮询上来需要先执行一次ajax查询:

  1. 第一步先有一个异步查询的方法,这里使用promise来模拟一个异步操作
const syncPromise = () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
          resolve();
      }, 50);
    })
 }
  1. 定一个延时的promise
const timeoutPromise = (delayTime) => {
    return new Promise(resolve => {
        window.timeId = setTimeout(resolve, delayTime);
    })
}
  1. 定义一个轮询,执行的异步方法使用第一步定义的promise,延时使用上面定义的timeoutPromise方法
const pollFetch = async () => {
    while (true) {
    const pollingTime = 120 * 1000;
    const delayTime = pollingTime + (1000 * parseInt(Math.random() * 40, 10));     // 轮询时间间隔
    try {
      await syncPromise();
      await timeoutPromise(delayTime);
    } catch (e) {
      await timeoutPromise(delayTime);
    }
  }
}

以上就是整个的轮询实现的方式,主要是利用了新的语言特性避免的嵌套回调,同时定义动态延时时间,避免所有的轮询同时到达服务器请求的情况,当然这只是我在实际遇到问题后的改进版本,也有不成熟的地方,大家有什么问题可以在评论区交流