前端如何优雅的使用定时器?

本文最后更新于:2 个月前

前言

在前端开发中,使用定时器可以处理一些循环操作或者延时操作,比如使用setinterval可以在指定的时间间隔重复执行函数,setTimeout则是用于在指定时间后执行一次函数,还有之前提到的requestAnimationFrame动画帧函数,在使用全局定时器时,可能不会关注其启用数量及优化操作,导致实际开发中遇到一些性能问题。借助本篇文章,与大家分享一个定时器的管理工具,希望各位看完后能有所收获。

对定时器的思考

工欲善其事必先利其器,从实现原理的角度,针对系统定时器,在这给出我的看法:

还记得之前写的事件循环文章吗?它的运行机制是不断监听调用栈和任务队列的状态,根据一定的规则来决定下一步要执行哪个任务,那么我们如何在JS中实现一个事件循环呢?

系统事件循环

首先是实现系统的事件循环操作,循环顾名思义就是循环的调用某个函数,我们可以用一个死循环实现类似队列的效果

const SystemLoop = (fn) => {
    while(true){
        fn()
    }
}
let count = 0
SystemLoop(() => {
    console.log(count++);
})

当然为了保证系统正常运行,我们还是会使用延时进行系统的其他任务模拟操作,防止控制台卡死

在node环境下使用以下代码实现一个系统循环的效果

const SystemLoop = (fn) => {
    // 延迟防止宕机
    setTimeout(() => {
        SystemLoop(fn)
        fn()
    })
}
let count = 0
SystemLoop(() => {
    console.log(count++);
})

效果同递归执行node中的nextTick和浏览器的requestAnimationFrame类似,循环执行某个函数,此时我们加上时间的判断,就可以实现setinterval或者settimeout的效果,下面是完整的interval的实现过程:

const CustomSetInterval = (fn, delay) => {
  let time = performance.now();
  SystemLoop(() => {
    const now = performance.now();
    if (now - time >= delay) {
      time = now;
      return fn();
    }
  });
};

const SystemLoop = (fn) => {
  while (true) {
    fn();
  }
};

let count = 0;
CustomSetInterval(() => {
  console.log(`${++count}秒后`);
}, 1000);

效果如下:

使用多个原生定时器

优点

就像react中的hooks,将逻辑分离在不同定时器中可以增加可读性,此外,通过不同的interval的id可以达到对某个任务的单独控制,易于管理和维护

缺点

以上面的定时器的实现为例,在全局创建较多的定时器会对性能造成一定的影响

于是基于上面的问题,我们是否可以将任务都放在一个interval中,以达到缓解资源分配过大的问题?

一个定时器执行多个任务

如果将多个任务集成到一个定时器时又会有什么问题?

优点

得利于异步,函数执行不会相互堵塞,函数的执行起始周期是统一的,其次是减少资源消耗,由于只有一个定时器,所以我们只需关注函数逻辑即可

缺点

其缺点也非常明显,就是无法单独对某个函数的循环进行停止,同一个定时器的所有任务都必须遵循相同的时间间隔,这无形中增加了代码量,使逻辑更复杂

进价思考

为了解决上述的问题,我们是不是可以在一个定时器多任务的思路上实现一个管理机制,同一个定时器中的函数使用集合来存储,通过控制集合来对函数进行管理,此外根据不同的时间间隔创建多个定时器,这样既可以解决上述一定时器多任务的问题,又达到了性能优化的问题

功能实现

有了上面的构思和思考,我们就可以开始着手于具体的代码设计和实现了

代码设计

如果了解上面说的浏览器和node循环机制,就不难理解,无论是settimeout,nextTick亦或是requestAnimationFrame,setImmediate,都是类似的实现方式,本质上是执行了某些任务之前或之后执行对应的循环函数,我们只需在其执行时传入一个递归函数,产生循环调用的效果,接着通过不同的delay对这些函数进行分类,就可以达到定时器管理的目的

定时器结构

首先我们需要提升定时器的维度,使用传入的delay来区分不同的定时器函数队列就像以下结构

// 定时器的函数列表
export type TimerItem = {
    id: string;
    handle: IHandle;
    delay?: IDelay;
};
// 定时器
export type Timer = {
    intervalId: IntervalId | Function;
    handles: TimerItem[];
    delay?: IDelay;
};

// 以延时时间为单位的多个定时器的集合
export type Timers = {
    [delay: IDelay]: Timer;
};

其中Timers表示需要创建几种定时器,Timer是一个hook的数组,存放这个时间周期的函数集合,数组的每一项就是TimerItem,用于描述函数相关信息结构

定时器管理器设计

在管理器中我们要实现interval的批量启动和停止的功能,后续可以通过startTimer和stopTimer将delay启动对应的定时器和停止定时器。此外,定时器需要运行在浏览器和node环境下,所以需要对环境进行处理。最后是添加,删除和清空单个定时器,分别使用add,delete,clear函数来实现

管理器的实现

使用以下代码实现一个TimerManager

import { requestFrame } from "utils-lib-js";
import { IDelay, IntervalId, IHandle, TimerItem, Timers, Timer, ITimerManagerOptions } from "./types"
// 默认定时器配置
export const defaultOptions: ITimerManagerOptions = {
    type: "interval",
    autoStop: true
};
// 定时器管理器
export class TimerManager {
    __id: number = 0;
    timers: Timers = {};
    readonly fixString = "#@$&*";
    readonly opts: ITimerManagerOptions;
    constructor(opts?: Partial<ITimerManagerOptions>) {
        // 使用默认选项填充缺失的属性
        this.opts = { ...defaultOptions, ...opts };
    }
    // 添加定时器项
    add(handle: IHandle, delay: IDelay = 0) {
        // 初始化指定延迟的定时器集合
        this.initDelay(delay);
        // 将定时器项推入定时器集合中
        return this.pushTimer(handle, delay);
    }
    // 删除定时器项
    delete(timer: TimerItem) {
        const { id, delay } = timer;
        const { timers } = this;
        if (delay && timers[delay]?.handles) timers[delay].handles = timers[delay].handles.filter((it) => it.id !== id);
    }
    // 停止并清除所有定时器
    clear() {
        const { timers } = this;
        Object.keys(timers).forEach((d) => this.stopTimer(timers[d]));
        this.timers = {};
    }
    // 将定时器项推入定时器集合中
    private pushTimer(handle: IHandle, delay: IDelay) {
        const { timers } = this;
        const __info = {
            id: this.getId(),
            handle,
            delay,
        };
        timers[delay].handles.push(__info);
        return __info;
    }
    // 初始化指定延迟的定时器集合
    private initDelay(delay: IDelay) {
        const { timers } = this;
        let timer = timers[delay];
        if (timer) return;
        // 创建新的定时器集合,并启动定时器
        timers[delay] = {
            intervalId: null,
            handles: [],
            delay,
        };
        this.startTimer(timers[delay]);
    }
    // 启动指定延迟的定时器
    startTimer(timer: Timer) {
        timer.intervalId = this.delayHandle(() => {
            this.autoStopTimer(timer)
            timer.handles.forEach((it) => it.handle());
        }, timer.delay);
    }
    // 停止指定延迟的定时器
    stopTimer(timer: Timer) {
        const { intervalId } = timer
        if (this.isFrame()) (intervalId as Function)();
        else clearInterval(intervalId as IntervalId);
    }
    // 自动释放定时器资源,根据当前定时器的handles长度判断
    private autoStopTimer(timer: Timer) {
        const { opts: { autoStop }, timers } = this
        const { delay } = timer
        if (autoStop && timer.handles.length <= 0) {
            this.stopTimer(timer)
            timers[delay] = null
        }
    }
    // 根据定时器返回延迟处理句柄
    delayHandle(handle: IHandle, delay: IDelay) {
        // 如果是帧定时器,则使用requestFrame方法,否则使用setInterval
        if (this.isFrame()) return requestFrame(handle, delay);
        else return setInterval(handle, delay);
    }
    // 判断是否为帧定时器
    isFrame = () => this.opts.type === "frame";
    // 获取唯一的定时器项id
    private getId() {
        return `${this.fixString}-${++this.__id}`;
    }
}

export default TimerManager;

使用方式

安装依赖

npm install timer-manager-lib
yarn add timer-manager-lib
pnpm install timer-manager-lib

引入并使用

以node环境下的ESModule为例

创建一个 Timer 实例:
import { TimerManager } from "timer-manager-lib";
const timerManager = new TimerManager({
  type: "interval", // interval 轮询定时器或 frame 帧定时器
  autoStop: true, // 当没有句柄时自动停止定时器
});
添加定时器

使用`add`方法添加定时器。它接受一个回调函数`handle`和一个间隔时间

const handle = () => {
  console.log("定时器触发");
};
const delay = 1000; // 时间以毫秒为单位
const timer = timerManager.add(handle, delay);
删除定时器

使用`delete`方法删除某项定时器,参数提供添加定时器返回的timer对象

timerManager.delete(timer);
清除所有定时器

使用`clear`方法停止并清除所有定时器

timerManager.clear();
启动、暂停对应delay的定时器

使用`startTimer`和`stopTimer`对某个interval启动、暂停

const timerManage = new TimerManager();
const { timers } = timerManage;
const timer1 = timerManage.add(() => {
  console.log("hello");
}, 1000);
timerManage.add(() => {
  console.log("阿宇的编程之旅");
}, 1000);
const { delay } = timer1;
timerManage.stopTimer(timers[delay]); // 暂停定时器
setTimeout(() => {
  timerManage.startTimer(timers[delay]); // 1.5秒后启动定时器
}, 1500);
自动停止

默认情况下`autoStop`选项设置为`true`。这意味着当没有句柄(timer的handlers 为空)时,定时器将自动停止。如果要禁用此行为,请在初始化时将`autoStop`设置为`false`。

使用效果

来看看下面代码的输出结果

const timerManager = new TimerManager()
const timer1 = timerManager.add(() => {
    console.log('i m timer1');
}, 1000)
timerManager.add(() => {
    console.log('i m timer2');
    timerManager.delete(timer1)
    console.log('time1 del');
}, 1000)
timerManager.add(() => {
    console.log('i m timer3');
    const { timers } = timerManager
    timerManager.stopTimer(timers[timer1.delay])
    console.log(timer1.delay, ` stop`);
}, 1000)
timerManager.add(() => {
    console.log('i m timer4');
    timerManager.clear()
    console.log('timer clear');
}, 3000)

至此,我们的定时器管理工具包就实现完毕

写在最后

本文主要讲述了一个定时器管理工具的构思和实现,通过回顾系统事件循环的原理:如何使用一个死循环实现类似队列的效果;通过加入时间判断,实现了类似setInterval和setTimeout的功能。接着,我们探讨了定时器的特点,以及改进方式,最终通过定时器管理器的设计及实现使定时器支持对不同延迟时间的定时器进行分类,可以动态添加、删除和清空。

以上就是文章全部内容了,如果觉得文章不错的话,还望三连支持一下,感谢!

相关代码:

定时器管理器

timer-manager-lib - npm

myCode: 基于js的一些小案例或者项目