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