在實時性需求場景中,比如電商訂單狀態跟蹤、物流信息更新、服務器監控面板,每隔固定時間拉取最新數據是高頻需求。看似簡單的“每5秒請求一次”,如果直接上手實現,很容易陷入資源浪費、接口雪崩、內存泄漏等坑。本文結合實際項目經驗,帶你從基礎實現逐步優化,打造穩定、高效、用户體驗佳的輪詢方案。

一、常見輪詢的“坑”你踩過嗎?

提到輪詢,很多開發者第一反應是用setInterval快速實現,但實際場景中會暴露諸多問題:

  • 接口響應慢於輪詢間隔時,前一次請求還未返回,下一次請求已發起,導致請求堆積、接口壓力劇增;
  • 頁面切換到後台(如打開新標籤頁、最小化瀏覽器)時,輪詢仍在持續,浪費用户帶寬和設備電量;
  • 網絡異常或接口報錯時,輪詢直接中斷,用户看不到最新數據;
  • 頁面銷燬後定時器未清理,導致內存泄漏,甚至觸發無效請求。

以電商訂單狀態查詢為例,假設接口響應時間平均1.5秒,用setInterval每5秒輪詢,若用户切換標籤頁10分鐘,會額外產生120次無效請求——這不僅是資源浪費,還可能觸發接口限流。

二、版本迭代:輪詢方案逐步優化

1. 第一版:基礎實現(能用但隱患重重)

最直觀的實現方式是使用setInterval,首次加載時執行一次,之後每5秒重複請求。

import { ref, onMounted, onUnmounted } from 'vue';

// 訂單狀態數據
const orderStatus = ref(null);
let timer = null;

onMounted(() => {
  // 輪詢核心函數
  const poll = () => {
    fetch('/api/order/status')
      .then(res => res.json())
      .then(data => {
        orderStatus.value = data;
      });
  };

  // 首次立即執行
  poll();
  // 每5秒輪詢
  timer = setInterval(poll, 5000);
});

// 頁面卸載時清理定時器
onUnmounted(() => {
  clearInterval(timer);
});

優點:代碼簡潔,快速實現核心功能; 缺點:未處理請求併發、網絡異常、頁面不可見等場景,僅適合臨時原型開發。

2. 第二版:可控輪詢(解決併發與異常問題)

針對第一版的缺陷,核心優化思路是“請求完成後再延遲發起下一次”,同時加入請求取消和異常處理機制。

import { ref, onMounted, onUnmounted } from 'vue';

const orderStatus = ref(null);
let abortController = null;

// 異步輪詢函數
const poll = async () => {
  try {
    // 取消上一次未完成的請求
    abortController?.abort();
    abortController = new AbortController();

    const res = await fetch('/api/order/status', {
      // 傳入取消信號
      signal: abortController.signal
    });

    // 處理HTTP錯誤狀態碼
    if (!res.ok) throw new Error(`請求失敗:${res.status}`);

    const data = await res.json();
    orderStatus.value = data;
  } catch (err) {
    // 忽略主動取消的錯誤,其他錯誤提示並繼續輪詢
    if (err.name !== 'AbortError') {
      console.warn('輪詢失敗,將重試:', err.message);
    }
  } finally {
    // 無論成功失敗,請求完成後延遲5秒發起下一次
    setTimeout(poll, 5000);
  }
};

onMounted(() => {
  // 啓動輪詢
  poll();
});

onUnmounted(() => {
  // 頁面卸載時取消請求
  abortController?.abort();
});

核心優化點

  • setTimeout替代setInterval,實現“串行輪詢”,避免請求併發堆積;
  • 藉助AbortControllerAPI,可主動取消未完成的請求,尤其適合頁面卸載時清理;
  • 捕獲網絡異常和HTTP錯誤,保證輪詢不中斷,提升穩定性。

3. 第三版:智能節流(結合頁面可見性)

用户切換標籤頁後,頁面處於不可見狀態,此時繼續輪詢毫無意義。利用瀏覽器visibilitychange事件,可實現“頁面可見時輪詢,隱藏時暫停”。

import { ref, onMounted, onUnmounted } from 'vue';

const orderStatus = ref(null);
let abortController = null;
let isPageVisible = true;

// 監聽頁面可見性變化
const handleVisibilityChange = () => {
  isPageVisible = !document.hidden;
  console.log(`頁面狀態:${isPageVisible ? '可見' : '隱藏'}`);
};

const poll = async () => {
  if (!isPageVisible) return;

  try {
    abortController?.abort();
    abortController = new AbortController();

    const res = await fetch('/api/order/status', {
      signal: abortController.signal
    });

    const data = await res.json();
    orderStatus.value = data;
  } catch (err) {
    if (err.name !== 'AbortError') {
      console.warn('輪詢失敗:', err);
    }
  } finally {
    // 僅在頁面可見時繼續輪詢
    if (isPageVisible) {
      setTimeout(poll, 5000);
    }
  }
};

onMounted(() => {
  // 註冊頁面可見性監聽
  document.addEventListener('visibilitychange', handleVisibilityChange);
  
  // 啓動輪詢
  poll();

  // 頁面從隱藏恢復可見時,立即觸發一次輪詢
  document.addEventListener('visibilitychange', () => {
    if (!document.hidden) {
      setTimeout(poll, 1000);
    }
  });
});

onUnmounted(() => {
  abortController?.abort();
  // 移除監聽,避免內存泄漏
  document.removeEventListener('visibilitychange', handleVisibilityChange);
});

智能優化點

  • 頁面隱藏時暫停輪詢,節省帶寬和設備資源;
  • 頁面恢復可見時,延遲1秒發起請求,避免多個頁面同時喚醒導致的接口峯值;
  • 移除監聽事件,防止內存泄漏。

4. 終極版:封裝可複用Hook(工程化落地)

將上述邏輯抽象為通用usePollingHook,支持傳入請求函數、輪詢間隔,適配不同業務場景,提升代碼複用性。

// 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 isPageVisible = true;

  const poll = async () => {
    if (!isPageVisible || 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('輪詢異常:', err);
      }
    } finally {
      loading.value = false;
      if (isPageVisible) {
        setTimeout(poll, interval);
      }
    }
  };

  // 啓動輪詢
  const start = () => {
    document.removeEventListener('visibilitychange', handleVisibility);
    document.addEventListener('visibilitychange', handleVisibility);
    poll();
  };

  // 停止輪詢
  const stop = () => {
    abortController?.abort();
    document.removeEventListener('visibilitychange', handleVisibility);
  };

  // 處理頁面可見性
  const handleVisibility = () => {
    isPageVisible = !document.hidden;
    if (isPageVisible) {
      setTimeout(poll, 1000);
    }
  };

  return { data, loading, error, start, stop };
}

使用方式:簡潔高效,適配各種輪詢場景

<script setup>
import { usePolling } from '@/composables/usePolling';

// 業務請求函數
const fetchOrderStatus = async (signal) => {
  const res = await fetch('/api/order/status', { signal });
  if (!res.ok) throw new Error('訂單狀態查詢失敗');
  return res.json();
};

// 初始化輪詢:每5秒請求一次
const { data: orderStatus, loading } = usePolling(fetchOrderStatus, 5000);

// 頁面掛載時啓動(也可手動控制啓動時機)
onMounted(() => {
  start();
});

// 頁面卸載時停止
onUnmounted(() => {
  stop();
});
</script>

<template>
  <div class="order-status">
    <div v-if="loading">加載中...</div>
    <div v-else-if="error">查詢失敗:{{ error.message }}</div>
    <div v-else>
      訂單狀態:{{ orderStatus?.status }}
      更新時間:{{ orderStatus?.updateTime }}
    </div>
  </div>
</template>

三、主流輪詢方案對比與選型建議

方案 實現方式 核心優勢 適用場景
setInterval 固定間隔觸發請求 代碼極簡,快速落地 臨時原型、無併發風險的簡單場景
串行setTimeout 請求完成後延遲觸發 避免併發,穩定性高 多數業務場景(如訂單查詢、數據監控)
WebSocket 服務端推送+客户端接收 實時性最高,減少請求次數 高頻更新場景
Server-Sent Events(SSE) 服務端單向流式推送 輕量實時,無需雙向通信 日誌流、系統通知、實時報表
智能輪詢(本文方案) 可見性控制+串行請求+取消機制 節能、穩定、用户體驗佳 生產環境通用場景(推薦)

選型建議

  • 若實時性要求極高(延遲<1秒):優先選WebSocket;
  • 若僅需服務端向客户端推送數據:選SSE(開發成本低於WebSocket);
  • 若接口為RESTful風格,且更新頻率中等(5秒+):選智能輪詢;
  • 若僅需快速驗證功能:用基礎setInterval(不推薦生產環境)。

四、進階場景:輪詢的靈活拓展

1. 動態調整輪詢頻率

網絡正常時按5秒輪詢,異常時自動降頻(如30秒),恢復後再提速:

// 在usePolling的finally中修改
let currentInterval = interval;
finally {
  loading.value = false;
  if (isPageVisible) {
    // 有錯誤時降頻,無錯誤時恢復原間隔
    currentInterval = error.value ? 30000 : interval;
    setTimeout(poll, currentInterval);
  }
}

2. 多接口錯峯輪詢

多個接口需輪詢時,避免同時發起請求:

// 組合多個請求,統一控制下一輪時機
const fetchMultipleData = async (signal) => {
  // 錯峯發起請求
  const [orderData, logisticsData] = await Promise.all([
    fetch('/api/order/status', { signal }),
    new Promise(resolve => setTimeout(() => resolve(fetch('/api/logistics')), 1000))
  ]);
  return { orderData: await orderData.json(), logisticsData: await logisticsData.json() };
};

3. 離線重連機制

網絡斷開時,採用指數退避策略重試:

let retryCount = 0;
finally {
  loading.value = false;
  if (isPageVisible) {
    if (error.value) {
      // 指數退避:1s→2s→4s→8s(最大8秒)
      retryCount++;
      const retryInterval = Math.min(1000 * Math.pow(2, retryCount), 8000);
      setTimeout(poll, retryInterval);
    } else {
      retryCount = 0;
      setTimeout(poll, interval);
    }
  }
}

五、總結:優雅輪詢的核心原則

實現“每X秒輪詢”的關鍵,不在於“按時發起請求”,而在於“聰明地發起請求”。優秀的輪詢方案需滿足:

  1. 避免併發:用串行setTimeout替代setInterval,防止請求堆積;
  2. 資源節省:結合頁面可見性API,減少無效請求;
  3. 異常容錯:捕獲錯誤不中斷輪詢,支持請求取消;
  4. 工程化:封裝為通用工具,適配多場景複用。