前端轮询如何设计?固定轮询、自适应轮询和退避有什么区别?
简化版
轮询是前端按一定间隔反复请求服务端,用来获取任务状态、消息数量或异步处理结果。设计时要避免固定频率无脑请求,应考虑页面可见性、错误退避、最大次数、用户离开取消、服务端压力和结果终态。固定轮询简单但浪费;自适应轮询会根据状态调整间隔;指数退避适合失败或服务繁忙时降压。
详细版
轮询适合“实时性要求不极致,但需要持续刷新”的场景。
| 策略 | 特点 | 场景 |
|---|---|---|
| 固定轮询 | 每 N 秒请求 | 简单状态刷新 |
| 自适应轮询 | 根据状态调整间隔 | 异步任务进度 |
| 指数退避 | 失败后逐渐拉长 | 服务繁忙或弱网 |
| 可见性暂停 | 页面后台停止或降频 | 节省资源 |
let stopped = false
async function poll() {
while (!stopped) {
const data = await fetchStatus()
if (data.done) break
await sleep(2000)
}
}
轮询不是 setInterval 写完就结束,取消、退避和终态才是工程重点。
完整版教学
一、轮询适合什么场景
轮询适合异步任务状态、导出进度、支付结果查询、通知数量、审核状态等场景。
它的优点是实现简单、兼容性好、服务端要求低。缺点是有延迟、浪费请求、服务端压力较大。
二、不要默认 setInterval
setInterval 容易出现请求堆叠:上一次请求还没回来,下一次又发出。
更好的方式是“请求完成后再等待下一轮”。
async function loop() {
while (running) {
await request()
await sleep(interval)
}
}
这样不会因为接口慢而堆积。
三、终态和最大次数
轮询必须知道什么时候停止。终态可能是成功、失败、取消、超时或超过最大次数。
| 终止条件 | 说明 |
|---|---|
| status = done | 任务完成 |
| status = failed | 任务失败 |
| maxAttempts | 避免无限轮询 |
| 页面离开 | 取消轮询 |
| 用户主动取消 | 停止请求 |
没有停止条件的轮询就是隐形性能问题。
四、错误退避
接口失败时不要继续高频请求。可以把间隔按指数增长。
const delay = Math.min(base * 2 ** failCount, maxDelay)
也可以加随机抖动,避免大量客户端同一时间重试。
五、页面可见性
用户切到后台时,轮询可以暂停或降频。
document.addEventListener('visibilitychange', () => {
if (document.hidden) pausePolling()
else resumePolling()
})
这样能节省用户电量和服务端资源。
六、和 WebSocket/SSE 的取舍
如果实时性要求高、服务端主动推送多,WebSocket 或 SSE 更合适。如果只是查任务状态,轮询更简单。
| 方案 | 优点 | 缺点 |
|---|---|---|
| 轮询 | 简单可靠 | 有延迟和浪费 |
| SSE | 服务端单向推送 | 连接管理 |
| WebSocket | 双向实时 | 架构复杂 |
七、常见误区与追问
- 误区:轮询就是 setInterval。 setInterval 容易请求堆叠,工程上常用完成后再调度下一轮。
- 误区:接口失败也按原频率请求。 失败时应退避,避免雪上加霜。
- 误区:用户离开页面轮询也没关系。 后台轮询浪费电量和服务端资源,应暂停或取消。
- 追问:如何避免请求堆叠? 等本轮请求完成并等待间隔后再发下一轮。
- 追问:轮询多久停? 由终态、最大次数、总超时时间和用户行为决定。
- 追问:什么时候不用轮询? 高频实时互动、双向通信更适合 WebSocket,单向实时消息可考虑 SSE。
八、加强记忆
轮询记成“问一次、等一下、再问”。重点是不要堆叠、要有终态、失败退避、后台降频,别让简单方案变成流量黑洞。