在做實時監(jiān)控系統(tǒng)時,比如服務(wù)器狀態(tài)面板、訂單處理中心或物聯(lián)網(wǎng)設(shè)備看板,每隔 5 秒自動拉取最新數(shù)據(jù)是再常見不過的需求了。
但你有沒有遇到過這些問題?
- 頁面切到后臺還在瘋狂發(fā)請求,浪費資源
- 上一次請求還沒回來,下一次又發(fā)了,接口雪崩
- 用戶切換標簽頁回來,發(fā)現(xiàn)數(shù)據(jù)“卡”在舊狀態(tài)
- 頁面銷毀了定時器還在跑,內(nèi)存泄漏
今天我就以一個運維監(jiān)控平臺的真實場景為例,帶你從“能用”做到“好用”。
一、問題場景:設(shè)備在線狀態(tài)輪詢
假設(shè)我們要做一個 IDC 機房設(shè)備監(jiān)控頁,需求如下:
- 每 5 秒查詢一次所有服務(wù)器的在線狀態(tài)
- 接口
/api/servers/status
響應(yīng)較慢(平均 1.2s) - 用戶可能切換到其他標簽頁處理郵件
- 頁面關(guān)閉時必須停止輪詢
如果直接寫個 setInterval
,很容易踩坑。我們一步步來優(yōu)化。
二、第一版:基礎(chǔ)輪詢(能跑,但有隱患)
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)
})
onUnmounted(() => {
clearInterval(timer)
})
? 實現(xiàn)了基本功能
? 但存在三個致命問題:
- 接口未完成就發(fā)起下一次請求 → 可能雪崩
- 頁面不可見時仍在輪詢 → 浪費帶寬和電量
- 異常未處理 → 網(wǎng)絡(luò)錯誤可能導(dǎo)致后續(xù)不再輪詢
三、第二版:可控輪詢 + 可見性優(yōu)化
我們改用“請求完成后再延遲 5 秒”的策略,避免并發(fā):
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 {
setTimeout(poll, 5000)
}
}
onMounted(() => {
poll()
})
onUnmounted(() => {
abortController?.abort()
})
?? 關(guān)鍵點解析:
finally
中 setTimeout
實現(xiàn)“串行輪詢”,避免并發(fā)AbortController
可在組件卸載時主動取消進行中的請求- 錯誤被捕獲后仍繼續(xù)輪詢,保證穩(wěn)定性
四、第三版:智能節(jié)流 —— 頁面可見性控制
現(xiàn)在解決“頁面不可見時是否輪詢”的問題。我們引入 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)
}
}, { once: true })
}
}
}
poll()
})
?? 這里做了兩層控制:
- 頁面隱藏時,不再自動發(fā)起下一輪請求
- 頁面重新可見時,延遲 1 秒觸發(fā)一次查詢,避免瞬間喚醒過多資源
五、封裝成可復(fù)用的輪詢 Hook
把這套邏輯抽象成通用 usePolling
Hook:
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>
六、對比主流輪詢方案
方案 | 實現(xiàn)方式 | 優(yōu)點 | 缺點 | 適用場景 |
---|
setInterval | 固定間隔觸發(fā) | 簡單直觀 | 不考慮響應(yīng)時間,易并發(fā) | 快速原型 |
串行 setTimeout | 請求完再延時 | 避免并發(fā),穩(wěn)定 | 周期不嚴格 | 多數(shù)業(yè)務(wù)場景 ? |
WebSocket | 服務(wù)端推送 | 實時性最高 | 成本高,兼容性差 | 股票行情、聊天 |
Server-Sent Events | 單向流式推送 | 輕量級實時 | 不支持 IE | 日志流、通知 |
智能輪詢(本方案) | 可見性+串行控制 | 節(jié)能、穩(wěn)定、用戶體驗好 | 略復(fù)雜 | 生產(chǎn)環(huán)境推薦 ? |
七、舉一反三:三個變體場景實現(xiàn)思路
動態(tài)輪詢頻率
如網(wǎng)絡(luò)異常時降頻至 30s 一次,正常后恢復(fù) 5s。可在 finally
中根據(jù) error.value
動態(tài)調(diào)整 setTimeout
時間。
多接口協(xié)同輪詢
多個 API 輪詢但希望錯峰發(fā)送。可用 Promise.all
組合請求,在 finally
統(tǒng)一控制下一輪時機,避免瞬間并發(fā)。
離線重連機制
當檢測到網(wǎng)絡(luò)斷開(fetch 超時),改為指數(shù)退避重試(1s → 2s → 4s → 8s),恢復(fù)后再切回 5s 正常輪詢。
小結(jié)
實現(xiàn)“每 5 秒輪詢”看似簡單,但要做到穩(wěn)定、節(jié)能、用戶體驗好,需要考慮:
- ? 使用 串行 setTimeout 替代 setInterval,避免請求堆積
- ? 利用 AbortController 主動取消無用請求
- ? 結(jié)合 頁面可見性 API 節(jié)省資源
- ? 封裝為 可復(fù)用 Hook,提升工程化水平
記住一句話:好的輪詢,是“聰明地少做事”,而不是“拼命做事情”。
下次當你接到“每隔 X 秒刷新”的需求時,別急著寫 setInterval
,先問問自己:用戶真的需要這么頻繁嗎?能不能用 WebSocket?頁面看不見的時候還要刷嗎?
?轉(zhuǎn)自https://juejin.cn/post/7530948113120624675
該文章在 2025/8/5 9:53:36 編輯過