数组实现定时器轮询的核心是将任务存入数组,每帧遍历执行并清理超时任务,支持once、repeat、id管理及性能优化。

用数组实现定时器轮询逻辑,核心是把待执行的回调任务存进数组,每帧或固定间隔遍历执行并清理已超时或完成的任务。这种方式轻量、可控,适合游戏循环、状态轮询、资源回收等场景,比频繁调用 setTimeout 或 setInterval 更易管理生命周期。
结构设计:任务数组 + 时间戳 + 状态标记
每个任务对象至少包含:回调函数(fn)、触发时间(dueTime)(毫秒时间戳)、是否只执行一次(once)、可选的 id 或标识符 用于查找/取消。不依赖外部计时器 ID,所有控制权在数组内。
- 初始化一个空数组,如
const timers = []; - 添加任务时推入对象:
timers.push({ fn: () => ..., dueTime: Date.now() + 3000, once: true }); - 主循环(如 requestAnimationFrame 或 setInterval)中遍历数组,检查
Date.now() >= task.dueTime - 命中后立即执行
task.fn(),再根据once决定是否splice删除
执行与清理:边遍历边移除,避免索引错乱
遍历时不能用 for...of 或正向 for 配合 splice,否则会跳过下一项。推荐倒序遍历或使用 filter 构建新数组。
- 倒序安全删除:
for (let i = timers.length - 1; i >= 0; i--) { if (Date.now() >= timers[i].dueTime) { timers[i].fn(); if (timers[i].once) timers.splice(i, 1); } } - 或批量过滤(适合高频率但任务不多):
timers = timers.filter(task => { if (Date.now() >= task.dueTime) { task.fn(); return !task.once; } return true; }); - 注意:若任务执行耗时较长,可在执行前记录开始时间,防止“时间漂移”累积
实战:封装 addTimer / clearTimer / clearAll 接口
把数组操作封装成方法,提升复用性与可读性。例如:
-
addTimer(fn, delayMs, options = { once: true }):自动计算dueTime = Date.now() + delayMs,返回唯一id(如自增数字或 Symbol) -
clearTimer(id):遍历匹配并移除对应任务(需在任务对象中保存id字段) -
clearAll():直接timers.length = 0或赋空数组 - 进阶可支持周期任务:
repeat: 1000表示每隔 1s 重复,此时不删,只更新dueTime += repeat
注意事项与优化点
这类轮询逻辑简单,但容易忽略边界问题:
- 避免在回调中同步修改同一数组(如又调
addTimer),建议统一在本轮遍历结束后再合并新增任务 - 高频轮询(如 60fps)下,单帧处理任务数过多会影响帧率,可限制每帧最多执行 N 个,剩余留到下一帧
- 时间精度依赖
Date.now(),在后台标签页中可能被节流;如需更高精度,可用performance.now()替代 - 调试时可加字段如
createdAt、execCount,方便日志追踪生命周期










