LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

如何优雅地实现每 5 秒轮询请求?

freeflydom
2025年8月5日 9:53 本文热度 51

在做实时监控系统时,比如服务器状态面板、订单处理中心或物联网设备看板,每隔 5 秒自动拉取最新数据是再常见不过的需求了。

但你有没有遇到过这些问题?

  • 页面切到后台还在疯狂发请求,浪费资源
  • 上一次请求还没回来,下一次又发了,接口雪崩
  • 用户切换标签页回来,发现数据“卡”在旧状态
  • 页面销毁了定时器还在跑,内存泄漏

今天我就以一个运维监控平台的真实场景为例,带你从“能用”做到“好用”。


一、问题场景:设备在线状态轮询

假设我们要做一个 IDC 机房设备监控页,需求如下:

  • 每 5 秒查询一次所有服务器的在线状态
  • 接口 /api/servers/status 响应较慢(平均 1.2s)
  • 用户可能切换到其他标签页处理邮件
  • 页面关闭时必须停止轮询

如果直接写个 setInterval,很容易踩坑。我们一步步来优化。


二、第一版:基础轮询(能跑,但有隐患)

import { ref, onMounted, onUnmounted } from 'vue'
const servers = ref([])
let timer = null
onMounted(() => {
  const poll = () => {
    fetch('/api/servers/status')
      .then(res => res.json())
      .then(data => {
        servers.value = data
      })
  }
  poll() // 首次立即执行
  timer = setInterval(poll, 5000) // 每5秒轮询
})
onUnmounted(() => {
  clearInterval(timer) // 🔍 清理定时器
})

✅ 实现了基本功能
❌ 但存在三个致命问题:

  1. 接口未完成就发起下一次请求 → 可能雪崩
  2. 页面不可见时仍在轮询 → 浪费带宽和电量
  3. 异常未处理 → 网络错误可能导致后续不再轮询

三、第二版:可控轮询 + 可见性优化

我们改用“请求完成后再延迟 5 秒”的策略,避免并发:

import { ref, onMounted, onUnmounted } from 'vue'
const servers = ref([])
let abortController = null // 用于取消请求
const poll = async () => {
  try {
    // 支持取消上一次请求
    abortController?.abort()
    abortController = new AbortController()
    const res = await fetch('/api/servers/status', {
      signal: abortController.signal
    })
    if (!res.ok) throw new Error('Network error')
    
    const data = await res.json()
    servers.value = data
  } catch (err) {
    if (err.name !== 'AbortError') {
      console.warn('轮询失败,将重试...', err)
    }
  } finally {
    // 🔍 请求结束后再等5秒发起下一次
    setTimeout(poll, 5000)
  }
}
onMounted(() => {
  poll() // 启动轮询
})
onUnmounted(() => {
  abortController?.abort()
})

🔍 关键点解析:

  • finally 中 setTimeout 实现“串行轮询”,避免并发
  • AbortController 可在组件卸载时主动取消进行中的请求
  • 错误被捕获后仍继续轮询,保证稳定性

四、第三版:智能节流 —— 页面可见性控制

现在解决“页面不可见时是否轮询”的问题。我们引入 visibilitychange 事件:

let isVisible = true
const handleVisibilityChange = () => {
  isVisible = !document.hidden
  console.log('页面可见性:', isVisible ? '可见' : '隐藏')
}
onMounted(() => {
  // 监听页面可见性
  document.addEventListener('visibilitychange', handleVisibilityChange)
  const poll = async () => {
    try {
      abortController?.abort()
      abortController = new AbortController()
      const res = await fetch('/api/servers/status', {
        signal: abortController.signal
      })
      const data = await res.json()
      servers.value = data
    } catch (err) {
      if (err.name !== 'AbortError') {
        console.warn('轮询失败:', err)
      }
    } finally {
      // 🔍 只有页面可见时才继续轮询
      if (isVisible) {
        setTimeout(poll, 5000)
      } else {
        // 页面隐藏,等待恢复后再请求
        document.addEventListener('visibilitychange', function waitVisible() {
          if (!document.hidden) {
            document.removeEventListener('visibilitychange', waitVisible)
            setTimeout(poll, 1000) // 恢复后1秒再查
          }
        }, { once: true })
      }
    }
  }
  poll()
})

🔍 这里做了两层控制:

  1. 页面隐藏时,不再自动发起下一轮请求
  2. 页面重新可见时,延迟 1 秒触发一次查询,避免瞬间唤醒过多资源

五、封装成可复用的轮询 Hook

把这套逻辑抽象成通用 usePolling Hook:

// composables/usePolling.js
import { ref } from 'vue'
export function usePolling(fetchFn, interval = 5000) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  let abortController = null
  let isVisible = true
  const poll = async () => {
    if (loading.value) return // 防止重复执行
    loading.value = true
    error.value = null
    try {
      abortController?.abort()
      abortController = new AbortController()
      const result = await fetchFn(abortController.signal)
      data.value = result
    } catch (err) {
      if (err.name !== 'AbortError') {
        error.value = err
        console.warn('Polling error:', err)
      }
    } finally {
      loading.value = false
      // 🔍 根据可见性决定是否继续
      if (isVisible) {
        setTimeout(poll, interval)
      }
    }
  }
  const start = () => {
    // 移除旧监听避免重复
    document.removeEventListener('visibilitychange', handleVisibility)
    document.addEventListener('visibilitychange', handleVisibility)
    poll()
  }
  const stop = () => {
    abortController?.abort()
    document.removeEventListener('visibilitychange', handleVisibility)
  }
  const handleVisibility = () => {
    isVisible = !document.hidden
    if (isVisible) {
      setTimeout(poll, 1000)
    }
  }
  return { data, loading, error, start, stop }
}

使用方式极其简洁:

<script setup>
import { usePolling } from '@/composables/usePolling'
const fetchStatus = async (signal) => {
  const res = await fetch('/api/servers/status', { signal })
  return res.json()
}
const { data, loading } = usePolling(fetchStatus, 5000)
// 自动在 onMounted 启动
</script>
<template>
  <div v-if="loading">加载中...</div>
  <ul v-else>
    <li v-for="server in data" :key="server.id">
      {{ server.name }} - {{ server.status }}
    </li>
  </ul>
</template>

六、对比主流轮询方案

方案实现方式优点缺点适用场景
setInterval固定间隔触发简单直观不考虑响应时间,易并发快速原型
串行 setTimeout请求完再延时避免并发,稳定周期不严格多数业务场景 ✅
WebSocket服务端推送实时性最高成本高,兼容性差股票行情、聊天
Server-Sent Events单向流式推送轻量级实时不支持 IE日志流、通知
智能轮询(本方案)可见性+串行控制节能、稳定、用户体验好略复杂生产环境推荐 ✅


七、举一反三:三个变体场景实现思路

  1. 动态轮询频率
    如网络异常时降频至 30s 一次,正常后恢复 5s。可在 finally 中根据 error.value 动态调整 setTimeout 时间。

  2. 多接口协同轮询
    多个 API 轮询但希望错峰发送。可用 Promise.all 组合请求,在 finally 统一控制下一轮时机,避免瞬间并发。

  3. 离线重连机制
    当检测到网络断开(fetch 超时),改为指数退避重试(1s → 2s → 4s → 8s),恢复后再切回 5s 正常轮询。


小结

实现“每 5 秒轮询”看似简单,但要做到稳定、节能、用户体验好,需要考虑:

  • ✅ 使用 串行 setTimeout 替代 setInterval,避免请求堆积
  • ✅ 利用 AbortController 主动取消无用请求
  • ✅ 结合 页面可见性 API 节省资源
  • ✅ 封装为 可复用 Hook,提升工程化水平

记住一句话:好的轮询,是“聪明地少做事”,而不是“拼命做事情”

下次当你接到“每隔 X 秒刷新”的需求时,别急着写 setInterval,先问问自己:用户真的需要这么频繁吗?能不能用 WebSocket?页面看不见的时候还要刷吗?

​转自https://juejin.cn/post/7530948113120624675


该文章在 2025/8/5 9:53:36 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved