开箱即用!轻量级轮询方案,支持同步获取轮询结果!

你害怕的不是未知的未来,而是不断重复错误的过去。

背景

在设计这个方案之前,以往我对于轮询的处理逻辑相对简单,就是基于setTimeout,根据轮询接口的返回结果,判断是否继续执行该轮询接口。

下面是临时写的示例代码(未经过测试),只是一个大致思路哈。

js 复制代码
let pollTimeout = null
let pollCount = 0
/** 轮询请求
 * @param {Object} options 选项
 * @param {number} options.timeout 超时时间
 * @param {number} options.interval 间隔时间
 * @param {Function} options.executor 执行方法
 * @param {Function} options.conditionFn 条件方法
 * @param {Function} options.successFn 成功方法
 * @param {Function} options.errorFn 错误方法
 * @return {void}
 */
export const pollRequest = ({timeout = 10000, interval = 1000, executor, conditionFn = () => false, successFn = () => {}, errorFn = () => {}} = {}) => {
  // 最大轮询次数
  const maxCount = Math.floor(timeout / interval)
  if (pollTimeout) {
    clearTimeout(pollTimeout)
  }
  const loop = async () => {
    if (pollCount >= maxCount) {
      errorFn('timeout')
      pollTimeout = null
      pollCount = 0
      return
    }
    const res = await executor()
    pollCount++
    if (conditionFn(res)) {
      successFn(res)
      pollTimeout = null
      pollCount = 0
    } else {
      errorFn(res)
      pollTimeout = setTimeout(loop, interval)
    }
  }
  loop()
}

// 示例接口
const getDemoUserList = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }])
    }, 1000)
  })
}

const pollTest = pollRequest({
  timeout: 10000,
  interval: 1000,
  executor: getDemoUserList,
  conditionFn: (res) => res.length > 0,
  successFn: () => {
    console.log('success')
  },
  errorFn: () => {
    console.log('error')
  }
})

pollTest()

之前一般都是采用callback的方案,通过传入回调方法来获取轮询结果。

直到这次又遇到了需要轮询的需求场景,就是在提交表单或者进入主页时都需要通过轮询一个状态接口,页面执行不同的操作。比如提交表单后,轮询状态接口,如果是正确状态就结束并跳转回主页,错误状态则继续轮询并且展示loading;

其实如果按前面的那种方案也能做,但是需要写成callback的形式,而且涉及改动的地方比较多(有20多处),为了让改动成本最小化,决定重构轮询方案。

该方案预期结果:用户在需要改动的模块中插入全局的状态轮询方法,通过async/await的方法,同步得到轮询结果,并根据结果来处理对应的逻辑。如下图

伪代码

js 复制代码
某业务模块
...
插入位置
const pollRes = await pollCheckStatus()
if (pollRes.status === 'fail') {
    ...轮询失败处理逻辑
    return
}
...轮询成功处理逻辑
...

设计思路

这次的设计思路中最主要的点是,将轮询操作的控制权提供给调用者,基于promise的resolve,我们可以将轮询处理逻辑进行缓存,之后根据调用者的实际业务逻辑可以更加灵活地处理轮询结果。

  • 不需要跟开头的方案那样,通过condition这个方法传入判断逻辑,并且还要规定方法返回结果只能是布尔值,那样做不够灵活。

  • 在新的方案中,我们会抛出两个方法,一个是success,一个是fail,这两个方法都可以结束轮询状态,并传入参数(便于在轮询结束后根据不同参数处理对应逻辑)。这个思路参考的promise设计规范 中的状态,该状态只有一次扭转机会,初始状态为pending ,之后只要调用 resolve 或者reject 任意一个方法,该状态就会扭转为最终态且不可再变动。这套逻辑映射到轮询的场景就是,只要处于pengding状态就一直轮询,一旦用户调用了success或者fail任意一个方法,状态扭转并且结束轮询。直到用户重启一轮新的轮询。

代码实现

根据这个思路,我设计了下面这个PollRequest

js 复制代码
const PENDING_RESULT = {
  PENDING: 'pending', // 轮询中
  SUCCESS: 'success', // 轮询成功
  FAIL: 'fail', // 轮询失败
}
const FAIL_TYPE = {
  TIMEOUT: 'timeout', // 轮询超时
  STOP: 'stop', // 轮询停止
}
/** 轮询失败类型对应的消息 */
const FAIL_TYPE_MSG = {
  [FAIL_TYPE.TIMEOUT]: '', // 轮询超时
  [FAIL_TYPE.STOP]: '', // 轮询停止消息
}

/** 轮询请求
 * @param {Object} options 配置参数
 * @param {boolean} options.enableTimeout 是否启用超时检测
 * @param {number} options.interval 轮询间隔时间 单位:毫秒
 * @param {number} options.timeout 轮询超时时间 单位:毫秒
 * @param {boolean} options.immediate 是否立即执行轮询
 * @return {PollRequest} 轮询请求实例
 */
export class PollRequest {
  constructor(options) {
    this.enableTimeout = options?.enableTimeout === false ? false : true
    this.interval = options?.interval || 1500
    this.timeout = options?.timeout || 10000
    this.immediate = options?.immediate || true // 是否立即执行轮询
    this.timer = null
    this.executeCount = 0 // 执行次数
    this.startTime = 0 // 记录轮询开始时间
    this.runResolve = null // 轮询resolve,后置执行,可以通过用户根据执行结果自行决定是否继续轮询
    this.pendingResult = PENDING_RESULT.PENDING // [pending|success|fail]
  }

  /*
   * 设置轮询配置
   * @param {Object} options 配置参数
   * @param {boolean} options.enableTimeout 是否启用超时检测
   * @param {number} options.interval 轮询间隔时间 单位:毫秒
   * @param {number} options.timeout 轮询超时时间 单位:毫秒
   */
  setConfig(options) {
    this.enableTimeout = options?.enableTimeout === false ? false : this.enableTimeout
    this.interval = options?.interval || this.interval
    this.timeout = options?.timeout || this.timeout
    this.immediate = options?.immediate || this.immediate
  }

  /*
   * 成功处理函数
   * @param {any} successResult 成功结果
   */
  success(successResult) {
    if (this.runResolve && this.pendingResult === PENDING_RESULT.PENDING) {
      this.pendingResult = PENDING_RESULT.SUCCESS
      this.resetPoll()
      this.runResolve({ status: PENDING_RESULT.SUCCESS, successResult })
    }
  }

  /*
   * 失败处理函数
   * @param {any} failResult 失败结果
   */
  fail(failResult) {
    if (this.runResolve && this.pendingResult === PENDING_RESULT.PENDING) {
      this.pendingResult = PENDING_RESULT.FAIL
      this.resetPoll()
      this.runResolve({ status: PENDING_RESULT.FAIL, failResult })
    }
  }

  /**
   * 启动轮询
   * @param {Function} executeFunc 执行函数
   * @return {Promise} 轮询结果 {status: 'success' | 'fail', successResult?: any, failResult?: any}
   */
  run(executeFunc) {
    return new Promise((resolve) => {
      this.resetPoll()
      this.startTime = Date.now()
      this.runResolve = resolve
      this.pendingResult = PENDING_RESULT.PENDING
  
      const proxyExecuteFunc = () => {
        if (this.pendingResult !== PENDING_RESULT.PENDING) return
        this.executeCount++
        executeFunc(this.executeCount)
      }
  
      const loop = () => {
        const nowTime = Date.now()
        // 计算轮询时长,超过超时时间则停止轮询,如果未启用超时检测,则不进行超时检测
        const diffTime = nowTime - this.startTime
        if (this.enableTimeout && diffTime >= this.timeout) {
          this.resetPoll()
          this.fail({failType: FAIL_TYPE.TIMEOUT, msg: FAIL_TYPE_MSG[FAIL_TYPE.TIMEOUT]})
          return
        }
        this.timer = setTimeout(() => {
          proxyExecuteFunc()
          loop()
        }, this.interval)
      }
      if (this.immediate) {
        proxyExecuteFunc()
      }
      loop()
    })
  }

  /*
   * 清除定时器
   */
  killTimer() {
    if (this.timer) {
      clearTimeout(this.timer)
      this.timer = null
    }
  }

  /*
   * 重置轮询状态,包括杀掉定时器进程
   */
  resetPoll() {
    this.startTime = 0
    this.executeCount = 0
    this.killTimer()
  }

  // 停止轮询,并清除相关状态
  stop() {
    this.resetPoll()
    this.fail({failType: FAIL_TYPE.STOP, msg: FAIL_TYPE_MSG[FAIL_TYPE.STOP]})
  }
}

实战场景

有了上面这个PollRequest类,接下来就是应用到开头提到的业务场景中去。需求是这样的,现在我进入小程序的商品详情页,提交订单并调用支付窗口完成支付,由于支付状态是后端从微信服务端获取结果,这个流程是异步的,所以采用轮询接口的方案来获取支付结果。

  1. 首先定义一个全局的支付状态轮询方法,方便统一管理
js 复制代码
// 轮询支付状态请求实例
const pollCheckPayStatusRequest = new PollRequest()

/**
   * 轮询支付状态请求实例
   * @param {string} orderId 订单ID
   * @return {Promise} 轮询结果 {status: 'success' | 'fail', successResult?: any, failResult?: any}
   */
  pollCheckPayStatus(orderId, options = {
    enableTimeout: false, // 默认不启用超时检测
    loadingMsg: '',
  }) {
    // 停止之前的轮询请求
    pollCheckPayStatusRequest.stop()

    const Http = require("../api/http"); // 兼容解决调用时找不到外层Http问题
    pollCheckPayStatusRequest.setConfig({
      enableTimeout: options?.enableTimeout || false,
      interval: options?.interval,
      timeout: options?.timeout
    })
    return pollCheckPayStatusRequest.run(() => {
      const app = getApp();
      const Toast = app.$toast()
      options.loadingMsg && Toast.loading(options.loadingMsg, true)
      Http.apiGetPayStatus({
        orderId
      }).then(res => {
        if (!res.data) {
          pollCheckPayStatusRequest.success(res)
        }
      }).catch(err => {
        pollCheckPayStatusRequest.fail(err)
      })
    })
  }
  1. 在对应模块中插入轮询逻辑
js 复制代码
// 提交支付逻辑
async submit(orderId) {
  Toast.loading('支付中...', true)
  try {
    // 调用支付窗口
    let { status: payStatus, msg: payMsg } = await app.wxPay(orderId)
    Toast.hideLoading()
    if (status !== 'success') throw(payMsg)
    
    // 调用轮询
    const pollRes = await app.pollCheckPayStatus(orderId, {
      loadingMsg: '数据更新中...'
    })
    let _failType = pollRes?.failResult?.failType
    let _status = pollRes.status
    if (_status === 'fail' && _failType == 'stop') return
    if (_status === 'fail') {
      throw(pollRes.failResult.msg)
    }
    Toast.none('支付成功')
    this.triggerEvent('update')
  } catch ({ msg }) {
    Toast.none(msg || '支付失败')
  }
}

上面就是在实际场景中的示例应用(伪代码逻辑),通过这种async/await的方式改造,可以提升代码的可读性,改造成本也比较低。

小结

这次的轮询改造方案还是挺有意义的,基于promise的设计,既可以使用链式调用promise.then.catch,也可以使用async/await的语法糖实现异步代码同步化,代码可以写得更加优雅~。另外就是借鉴的promise关于状态扭转的思路,让轮询状态的状态可以后置并抛出给调用者,使其比callback那种受限制的方式要更加灵活。

最后,各位看官可以在评论区发表自己的意见,大家一起友好探讨~~

相关推荐
3824278272 小时前
python:正则表达式
前端·python·正则表达式
用户47949283569152 小时前
我是怎么把模型回复用tts播放的更自然的
前端
JS_GGbond2 小时前
前端崩溃监控:给网页戴上“生命体征监测仪”
前端
俊劫2 小时前
AI 编码技巧篇(内部分享)
前端·javascript·ai编程
Maxkim2 小时前
一文读懂 Chrome CRX📦:你需要了解的核心知识点
前端·前端工程化
JackJiang2 小时前
AI大模型爆火的SSE技术到底是什么?万字长文,一篇读懂SSE!
前端·websocket
Mr_chiu2 小时前
数据可视化大屏模板:前端开发的效率革命与架构艺术
前端
进击的野人2 小时前
一个基于 Vue 的 GitHub 用户搜索案例
前端·vue.js·前端框架
ZsTs1192 小时前
《2025 AI 自动化新高度:一套代码搞定 iOS、Android 双端,全平台 AutoGLM 部署实战》
前端·人工智能·全栈