前言
我们想写一个功能,能够不停的添加计时任务执行,且每个任务都支持自定义延迟时间做某件事件,平时我们可能直接setTimeout就完事了,有多个少个任务开启多少个setTimeout,可是这样真的行么,同时开启这么多timeout不仅仅延迟会很大,性能也会有折扣
实际上我们只使用一个setTimeout计时器,就可以实现这种多个自定义延迟时间的任务功能
实现步骤
只需要下面的步骤即可:
- 待办任务处理:将待办任务存放到队列中,根据延迟时间计算出最终执行时间戳,任务可有序,可无序,处理方式不同,推荐有序,处理起来更简单快速(这里也是按照有序处理)
- 延迟处理:执行延迟任务时,满足执行条件的最小时间戳先执行,然后观察后续最小的时间戳时间间隔,setTimeout计时器根据时间差设置延迟,以此类推
- 任务执行实际:任务放入队列的时候,如果没设置延迟,直接执行即可,如果设置了延迟则入队,如果是队列第一个,则需要设置延迟计时器,当然后续进入的计时器抢到了第一个,还需要更新计时器,因此需要清理过去的计时器,重新计时;如果不是,则什么都不需要处理
代码实现
实现代码如下所示,我们写一个简易的任务管理系统
js
class Task {
// delay;
// timestamp;
// fn;
constructor(fn, delay = 0) {
this.fn = fn;
this.delay = delay;
this.timestamp = Date.now() + delay;
}
}
class Queue {
constructor() {
this.queue = [];
}
enter(task) {
let idx = 0;
for (const item of this.queue) {
if (item.timestamp > task.timestamp) {
break;
}
idx++;
}
this.queue.splice(idx, 0, task);
return idx;
}
leave() {
return this.queue.shift();
}
}
class TaskManager {
constructor() {
this.queue = new Queue();
this.timeout = null;
}
addTask(task) {
if (!task.delay) {
task.fn && task.fn();
return;
}
if (this.queue.enter(task) === 0) {
//新增加的非立即执行最早的任务,回更新timeout计时器延迟,因此需要清理之前的计时器
this.timeout && clearTimeout(this.timeout);
this.doDelayTask(task.delay);
}
}
doDelayTask(delay) {
this.timeout = setTimeout(() => {
let next = null;
do {
const current = this.queue.leave();
current.fn && current.fn();
next = this.queue.queue[0];
if (!next) break;
const delay = next.timestamp - Date.now();
if (delay > 0) {
this.doDelayTask(delay);
break;
}
} while (next);
}, delay);
}
}
执行以下测试案例
js
let first = Date.now();
function testInterval() {
let idx = 1;
let timeout = setInterval(() => {
console.log("time", idx++, "间隔", Date.now() - first);
if (idx > 100) {
clearInterval(timeout);
}
}, 1000);
}
function testManager() {
const manager = new TaskManager();
manager.addTask(
new Task(() => {
console.log("task1,1s后执行", Date.now() - first);
}, 1000),
);
manager.addTask(
new Task(() => {
console.log("task2,1s后执行", Date.now() - first);
}, 1000),
);
manager.addTask(
new Task(() => {
console.log("task3,3s后执行", Date.now() - first);
}, 3000),
);
manager.addTask(
new Task(() => {
console.log("task4,5s后执行", Date.now() - first);
}, 5000),
);
manager.addTask(
new Task(() => {
console.log("task5,7s后执行", Date.now() - first);
}, 7000),
);
manager.addTask(
new Task(() => {
console.log("task6,9s后执行", Date.now() - first);
}, 9000),
);
}
//计时用于更好观看结果
testInterval();
testManager();
使用vscode的nodejs环境打印如下
js
time 1 间隔 1004
task1,1s后执行 1008
task2,1s后执行 1008
time 2 间隔 2006
task3,3s后执行 3014
time 3 间隔 3014
time 4 间隔 4017
task4,5s后执行 5009
time 5 间隔 5025
time 6 间隔 6030
task5,7s后执行 7007
time 7 间隔 7038
time 8 间隔 8044
task6,9s后执行 9005
time 9 间隔 9050
time 10 间隔 10058
也可以看到nodejs的setInterval的机制和浏览器的不一样,nodejs的会持续叠加时间间隔,而浏览器环境会修正(个人测试了哈)
优化建议
上面的优化相对比较好优化的就是那个队列了,可以换成链表的方式,理论应该能够提一点速度,使用比较稳定
当然,这个实际就有点考验自己的基本功了(如果平时不怎么碰算法的智慧写业务的话),实际也不难
原理大概就是,如果操作很频繁的话,那么频繁操作数组则性能则会相对低一些,毕竟数组线性的,频繁插入删除,数组可能频繁创建和移动,相比较链表频发插入删除场景,链表理论效率更高
我们写一个吧,当然可以用可不用,理论和实践还是有差距的,只是演示一个思路
js
//链表
class linkedList {
constructor() {
this.linkedList = null;
}
enter(task) {
//空链直接给表头
if (!this.linkedList) {
this.linkedList = task;
return 0;
}
//只有一个节点,且新节点时间戳小于当前节点时间戳,则新节点成为表头
let current = this.linkedList;
if (task.timestamp < current.timestamp) {
this.linkedList = task;
task.next = current;
return 0;
}
//新节点不会成为表头,遍历链表找到插入位置
let idx = 1;
let pre = current;
let current = pre.next;
while (current) {
if (task.timestamp < current.timestamp) {
pre.next = task;
task.next = current;
return idx;
}
pre = current;
current = current.next;
idx++;
}
//到最后了
pre.next = task;
return idx;
}
leave() {
if (!this.linkedList) return
this.linkedList = this.linkedList.next;
}
}
最后
就介绍到这里吧,我们写点小东西,也避免被ai的洪流吃掉我们思考的脑子😄