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

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

前言

在前端开发中,使用定时器可以处理一些循环操作或者延时操作,比如使用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的功能。接着,我们探讨了定时器的特点,以及改进方式,最终通过定时器管理器的设计及实现使定时器支持对不同延迟时间的定时器进行分类,可以动态添加、删除和清空。

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

相关文章
|
前端开发 JavaScript
前端基础 - JavaScript定时器
前端基础 - JavaScript定时器
61 0
|
2月前
|
前端开发 JavaScript
前端基础(十三)_定时器(间歇定时器、延迟定时器)
本文介绍了JavaScript中定时器的使用,包括`setTimeout`和`setInterval`两种类型。`setTimeout`是实现延迟执行,即等待一定时间后执行一次指定的函数;而`setInterval`是实现间歇执行,即每隔一定时间就执行一次指定的函数。文章还介绍了如何使用`clearTimeout`和`clearInterval`来取消定时器的执行,并通过示例代码展示了定时器的创建和取消。
93 4
前端基础(十三)_定时器(间歇定时器、延迟定时器)
|
6月前
|
前端开发 JavaScript 程序员
(前端面试题)详解 JS 的 setTimeout 和 setInterval 两大定时器
(前端面试题)详解 JS 的 setTimeout 和 setInterval 两大定时器
150 0
|
前端开发 JavaScript 数据可视化
javascript逐行显示数据及php实时输出前端内容后台保持继续运行的解决方案(setTimeout定时器、flush和ob_flush函数、安装进度展示)
javascript逐行显示数据及php实时输出前端内容后台保持继续运行的解决方案(setTimeout定时器、flush和ob_flush函数、安装进度展示)
196 0
|
移动开发 前端开发 JavaScript
PHP定时更新数据库,定时器,定时任务详细讲解(通过前端触发)
PHP定时更新数据库,定时器,定时任务详细讲解(通过前端触发)
678 0
|
28天前
|
存储 人工智能 前端开发
前端大模型应用笔记(三):Vue3+Antdv+transformers+本地模型实现浏览器端侧增强搜索
本文介绍了一个纯前端实现的增强列表搜索应用,通过使用Transformer模型,实现了更智能的搜索功能,如使用“番茄”可以搜索到“西红柿”。项目基于Vue3和Ant Design Vue,使用了Xenova的bge-base-zh-v1.5模型。文章详细介绍了从环境搭建、数据准备到具体实现的全过程,并展示了实际效果和待改进点。
118 2
|
28天前
|
JavaScript 前端开发 程序员
前端学习笔记——node.js
前端学习笔记——node.js
36 0
|
28天前
|
人工智能 自然语言处理 运维
前端大模型应用笔记(一):两个指令反过来说大模型就理解不了啦?或许该让第三者插足啦 -通过引入中间LLM预处理用户输入以提高多任务处理能力
本文探讨了在多任务处理场景下,自然语言指令解析的困境及解决方案。通过增加一个LLM解析层,将复杂的指令拆解为多个明确的步骤,明确操作类型与对象识别,处理任务依赖关系,并将自然语言转化为具体的工具命令,从而提高指令解析的准确性和执行效率。
|
28天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
28天前
|
机器学习/深度学习 弹性计算 自然语言处理
前端大模型应用笔记(二):最新llama3.2小参数版本1B的古董机测试 - 支持128K上下文,表现优异,和移动端更配
llama3.1支持128K上下文,6万字+输入,适用于多种场景。模型能力超出预期,但处理中文时需加中英翻译。测试显示,其英文支持较好,中文则需改进。llama3.2 1B参数量小,适合移动端和资源受限环境,可在阿里云2vCPU和4G ECS上运行。