前言
你是否也曾在大屏项目中陷入这样的困境:
- 为了实时展示数据,页面里塞了十几个
setInterval - 定时器清理不当导致内存泄漏,页面越用越卡
- 切换页面后轮询还在疯狂请求,控制台报错刷屏
- 想统一控制所有定时任务的启停,却要逐个修改代码
作为一个常年和数据大屏打交道的前端开发者,我太懂这种痛了。大屏项目往往需要同时维护 N 个定时任务(数据刷新、状态同步、心跳检测...),原生定时器 API 用起来不仅繁琐,还容易出各种幺蛾子。
今天就给大家分享一个我专门为大屏场景设计的usePolling Hooks,用它管理定时任务,效率直接翻倍!
为什么需要专门的轮询 Hooks?
先说说大屏项目的特殊性:
- 高频定时任务多:一个大屏可能需要同时维护 5-10 个不同的轮询任务(实时数据、告警、状态更新等)
- 资源敏感:大屏通常需要长时间运行,内存泄漏是致命问题
- 可见性需求:页面隐藏时没必要继续轮询(比如切换标签页)
- 统一管理:需要能批量控制所有轮询的启停状态
原生setInterval的问题就很明显了:每次使用都要手动管理定时器 ID、手动清理、手动处理异常,代码冗余且容易出错。
于是我封装了这个usePolling Hooks,把所有重复逻辑收归一处,让业务代码只关注核心逻辑。
核心实现:usePolling.ts
先上代码,核心逻辑不到 50 行,却解决了大屏轮询的所有痛点:
import { ref, onMounted, onUnmounted } from "vue";
// interval: 轮询间隔时间,默认5分钟
export function usePolling(fn: () => void, interval: number = 5 * 60 * 1000) {
const timer = ref<number | null>(null);
const isActive = ref(true); // 页面可见性控制
const isPolling = ref(false);
const startPolling = () => {
if (isPolling.value) return; // 防止重复启动
isPolling.value = true;
if (timer.value) clearInterval(timer.value); // 清理旧定时器
// 设置新定时器
timer.value = window.setInterval(async () => {
if (!isActive.value) return; // 页面不可见时不执行
try {
await fn(); // 执行轮询任务(支持异步)
} catch (error) {
console.error("轮询异常:", error); // 统一错误处理
}
}, interval);
fn(); // 立即执行一次(大屏通常需要即时数据)
};
// 页面可见性变化时自动暂停/恢复
const handleVisibilityChange = () => {
isActive.value = !document.hidden;
};
// 组件挂载时启动轮询并监听可见性变化
onMounted(() => {
document.addEventListener("visibilitychange", handleVisibilityChange);
startPolling();
});
// 组件卸载时清理资源
onUnmounted(() => {
if (timer.value !== null) {
clearInterval(timer.value);
}
document.removeEventListener("visibilitychange", handleVisibilityChange);
});
return { startPolling };
}
大屏项目中的实战用法
使用起来更是简单到离谱,只需要两步:
- 定义你的轮询任务(比如请求数据)
- 用
usePolling包装它,指定间隔时间
<template>
<div class="dashboard">
<!-- 大屏内容 -->
</div>
</template>
<script setup lang="ts">
import { usePolling } from "./hooks/usePolling";
import { fetchRealTimeData } from "./api/dashboard";
// 1. 定义轮询任务:获取实时数据并更新视图
const fetchData = async () => {
const data = await fetchRealTimeData();
// 更新组件状态...
};
// 2. 使用轮询Hooks:每30秒刷新一次
const { startPolling } = usePolling(fetchData, 30 * 1000);
// 如需手动控制(比如按钮触发重启)
// const handleRestart = () => {
// startPolling();
// };
</script>
如果页面有多个轮询任务,直接多次调用即可:
// 任务1:每30秒刷新实时数据
usePolling(fetchRealTimeData, 30 * 1000);
// 任务2:每5分钟同步一次配置
usePolling(syncConfig, 5 * 60 * 1000);
// 任务3:每10秒检测一次告警状态
usePolling(checkAlarm, 10 * 1000);
所有轮询都会自动遵守页面可见性规则,组件卸载时自动清理,再也不用手动管理一堆定时器了!
扩展
在后续的开发过程中,我和团队的小伙伴意识到了usePolling还有不足之处,所以我又对其进行了进一步的优化和扩展
1. 控制任务并发:避免轮询任务「叠加执行」
在执行轮询任务
fn的时候,执行的耗时超过了轮询间隔(比如接口响应慢),setInterval会无视前一次任务是否完成,继续触发下一次执行,导致多个任务同时运行(并发),堆积大量请求 / 计算,占用过多资源。
优化方案: 用 setTimeout 递归调用替代 setInterval,确保上一次任务执行完成后,再开始计算下一次的间隔时间,避免并发。
export function usePolling(fn: () => void, interval: number = 5 * 60 * 1000) {
const timer = ref<number | null>(null);
const isActive = ref(true);
const isPolling = ref(false);
const isTaskRunning = ref(false); // 标记任务是否正在执行
// 递归执行任务的核心函数
const runTask = async () => {
if (!isActive.value || !isPolling.value) return; // 非激活状态或未启动轮询,终止递归
if (isTaskRunning.value) return; // 上一次任务未完成,跳过
isTaskRunning.value = true;
try {
await fn(); // 等待任务完成
} catch (error) {
console.error("轮询异常:", error);
} finally {
isTaskRunning.value = false;
// 任务完成后,间隔指定时间再执行下一次
timer.value = window.setTimeout(runTask, interval);
}
};
const startPolling = () => {
if (isPolling.value) return;
isPolling.value = true;
runTask(); // 启动递归
};
// 停止轮询的方法(新增)
const stopPolling = () => {
isPolling.value = false;
if (timer.value) {
clearTimeout(timer.value);
timer.value = null;
}
};
onMounted(() => {
document.addEventListener("visibilitychange", () => {
isActive.value = !document.hidden;
});
startPolling();
});
onUnmounted(() => {
stopPolling(); // 卸载时停止轮询
document.removeEventListener("visibilitychange", () => {
isActive.value = !document.hidden;
});
});
return { startPolling, stopPolling };
}
效果:确保同一时间只有一个轮询任务在运行,避免接口 / 计算资源被过度占用。
2. 优化初始执行时机:避免「同步阻塞」
原实现中
startPolling会同步执行fn(),如果fn是耗时的同步操作(或异步操作未等待),可能阻塞组件初始化(比如大屏首次渲染时)。
优化方案:初始执行也纳入异步流程,避免阻塞,并确保初始执行完成后再开始计时。
const startPolling = () => {
if (isPolling.value) return;
isPolling.value = true;
// 初始执行也通过 runTask 触发(异步处理)
runTask();
};
效果: 组件初始化时不会被轮询任务阻塞,提升首屏渲染速度。
3. 支持「动态调整间隔」:适配场景化需求
固定间隔可能不灵活 —— 比如大屏某些时段需要高频刷新(如高峰期),某些时段需要低频刷新(如凌晨)。
优化方案:允许动态修改轮询间隔,通过 ref 传入间隔时间,监听变化并重启轮询
// 改为接收 ref 类型的间隔,支持动态修改
export function usePolling(
fn: () => void,
interval: Ref<number> = ref(5 * 60 * 1000)
) {
// ... 其他状态
// 监听间隔变化,自动重启轮询
watch(interval, () => {
if (isPolling.value) {
stopPolling(); // 先停止
startPolling(); // 再用新间隔启动
}
});
// ... 剩余逻辑不变
}
//使用示例
const pollInterval = ref(30000); // 初始30秒
usePolling(fetchData, pollInterval);
// 某时刻动态修改为1分钟
pollInterval.value = 60000;
效果:无需手动停止 / 启动,间隔自动生效,适配动态场景。
4. 强化「暂停 / 恢复」能力:减少无效执行
原实现仅通过页面可见性控制暂停,但大屏场景可能需要手动暂停(如用户操作时、数据加载中)。
优化方案:新增 pause 和 resume 方法,允许手动控制轮询状态,减少不必要的执行。
export function usePolling(fn: () => void, interval: number = 5 * 60 * 1000) {
// ... 其他状态
const isPaused = ref(false); // 手动暂停标记
const runTask = async () => {
if (!isActive.value || !isPolling.value || isPaused.value) return;
// ... 剩余逻辑不变
};
// 手动暂停
const pause = () => {
isPaused.value = true;
};
// 手动恢复
const resume = () => {
isPaused.value = false;
if (isPolling.value) {
runTask(); // 立即执行一次,或等待下一次间隔(根据需求选择)
}
};
return { startPolling, stopPolling, pause, resume };
}
使用场景:
- 大屏有「手动刷新」按钮时,点击后暂停轮询,避免接口冲突
- 数据加载中(如显示 loading)时,暂停轮询,防止重复请求
5. 错误处理增强:避免「静默失败」与「过度重试」
原实现仅打印错误,但未处理「连续失败」场景(比如接口连续报错时,频繁重试会浪费资源)。
优化方案:
- 允许用户传入错误回调,自定义错误处理(如上报、提示)
- 增加失败重试限制(如连续失败 N 次后暂停,避免无效请求)
// 新增错误回调和重试限制参数
export function usePolling(
fn: () => void,
interval: number = 5 * 60 * 1000,
options: {
onError?: (error: Error) => void; // 自定义错误处理
maxRetry?: number; // 最大连续失败次数
} = {}
) {
const { onError, maxRetry = 3 } = options;
const failCount = ref(0); // 连续失败计数
const runTask = async () => {
// ... 其他判断
try {
await fn();
failCount.value = 0; // 成功后重置失败计数
} catch (error) {
failCount.value++;
onError?.(error as Error); // 触发自定义错误处理
console.error("轮询异常:", error);
// 超过最大失败次数,暂停轮询
if (failCount.value >= maxRetry) {
stopPolling();
console.warn(`连续失败${maxRetry}次,已暂停轮询`);
return;
}
} finally {
// ... 继续递归
}
};
// ... 剩余逻辑不变
}
效果:减少错误场景下的无效请求,同时让错误处理更灵活。
6. 清理机制强化:避免「内存泄漏」
虽然原实现有
onUnmounted清理,但如果组件在轮询过程中被频繁挂载 / 卸载,可能因清理不及时导致定时器残留。
优化方案:
- 在
startPolling前强制清理旧定时器,确保状态一致 - 用
onBeforeUnmount提前清理(比onUnmounted更早执行)
onBeforeUnmount(() => {
stopPolling();
document.removeEventListener("visibilitychange", handleVisibilityChange);
});
const startPolling = () => {
if (isPolling.value) return;
// 启动前强制清理旧定时器,防止残留
if (timer.value) {
clearTimeout(timer.value);
timer.value = null;
}
isPolling.value = true;
runTask();
};
效果:进一步降低内存泄漏风险,尤其适合大屏中组件频繁切换的场景。
优化后的全部代码
import { ref, onMounted, onUnmounted } from "vue";
// 轮询配置项类型(全部可选)
interface PollingOptions {
/** 错误处理回调 */
onError?: (error: Error) => void;
/** 最大连续失败次数(超过后暂停),默认无限制 */
maxRetry?: number;
/** 是否在启动时立即执行一次,默认true */
immediate?: boolean;
}
/**
* 轮询钩子函数
* @param fn 轮询执行的函数(支持异步)
* @param interval 轮询间隔时间(毫秒),默认5分钟
* @param options 轮询配置项(可选,可完全不传递)
* @returns 轮询控制方法
*/
export function usePolling(
fn: () => Promise<void> | void,
interval: number = 5 * 60 * 1000,
options: PollingOptions = {} // 可选参数,默认空对象
) {
const timer = ref<number | null>(null);
const isActive = ref(true); // 页面可见性控制
const isPolling = ref(false);
const failCount = ref(0); // 连续失败计数器
// 解析配置项(带默认值,避免用户未传递的情况)
const {
onError = (err) => console.error("轮询异常:", err), // 默认错误处理
maxRetry = Infinity, // 默认无限制重试
immediate = true, // 默认立即执行
} = options;
// 执行轮询任务
const executeTask = async () => {
if (!isActive.value) return;
try {
await fn();
failCount.value = 0; // 成功则重置失败计数
} catch (error) {
failCount.value++;
onError(error as Error);
// 超过最大重试次数则暂停
if (failCount.value >= maxRetry) {
console.warn(`已连续失败${maxRetry}次,暂停轮询`);
stopPolling();
}
}
};
// 启动轮询
const startPolling = () => {
if (isPolling.value) return;
isPolling.value = true;
clearTimer();
// 立即执行一次
if (immediate) {
executeTask();
}
// 设置定时器
timer.value = window.setInterval(executeTask, interval);
};
// 暂停轮询
const pausePolling = () => {
if (!isPolling.value) return;
clearTimer();
};
// 恢复轮询
const resumePolling = () => {
if (!isPolling.value) return;
clearTimer();
timer.value = window.setInterval(executeTask, interval);
};
// 停止轮询
const stopPolling = () => {
isPolling.value = false;
clearTimer();
};
// 清理定时器
const clearTimer = () => {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
};
// 页面可见性变化处理
const handleVisibilityChange = () => {
const prevActive = isActive.value;
isActive.value = !document.hidden;
// 页面从隐藏变为可见时,若轮询中则立即执行一次
if (!prevActive && isActive.value && isPolling.value) {
executeTask();
}
};
// 生命周期钩子
onMounted(() => {
document.addEventListener("visibilitychange", handleVisibilityChange);
startPolling();
});
onUnmounted(() => {
stopPolling();
document.removeEventListener("visibilitychange", handleVisibilityChange);
});
return {
startPolling,
pausePolling,
resumePolling,
stopPolling,
isPolling, // 暴露轮询状态
};
}