哨兵节点实现的自驱式任务队列,不废话,上码:
javascript
/** @typedef { ReturnType<typeof newQue> } que */
/** @typedef{ () => Promise<any> } task */
/** @typedef{ string | number } key */
/** @typedef{ (...rest:any)=>void } callback */
// map :映射表,对外不可见,用 key 作唯一标识,映射出各个任务队列
// 队列特点:自入、自驱、自毁
// 队列中的任务:不同队列之间并行,同一队列内部串行
const map = /** @type{ Map<key, que> } */(new Map());
const getQue = (/** @type{ key } */ key) => map.get(key);
const newQue = (/** @type{ key } */ key, /** @type{ callback } */cb) => {
// 自环哨兵节点,是实现链表的巅峰模式,对外不可见
const sentinel = Object.create(null); // sentinel自身代表空
sentinel.next = sentinel; // 链表头 一定要写字面量sentinel.next
sentinel.prev = sentinel; // 链表尾 一定要写字面量sentinel.prev
const mod = Object.create(null); // mod=module,用于最终返回queue对象
map.set(key, mod); // 自入:入映射表
const del = () => map.delete(key);
const push = (/** @type{ task } */ task) => {
if (sentinel.next === sentinel) { Promise.resolve().then(run) } // 自驱:此时链表为空,但呆会即有任务入驻,异步启动
const node = Object.create(null); // 新节点
node.task = task;
node.prev = sentinel.prev; // 新节点的前指针 指向 链表尾(sentinel.prev)
node.next = sentinel; // 新节点的后指针 指向 空(sentinel自身代表空)
sentinel.prev.next = node; // 链表尾的后指针 指向 新节点
sentinel.prev = node; // 链表尾 赋值为 新节点
};
const shift = () => {
const node = sentinel.next; // 得到头结点
if (node === sentinel) return;
node.prev.next = node.next; // 删除节点
node.next.prev = node.prev;
};
const peek = () => sentinel.next !== sentinel ? /** @type{ task } */(sentinel.next.task) : void (0);
const run = () => {
const task = peek(); // 得到头结点任务
if (!task) { // 循环终结条件:链表空
del(); // 自毁:出映射表
if (cb) { cb() } // 回调:至少可以让外部感知队列完成
return;
}
task() // 执行任务
.finally(shift) // 执行完毕,出列
.finally(run) // 下一个
; // 异步递归:实现大循环(函数级),串行执行链表结点中的任务
};
Object.assign(mod, { push }); // push方法,精简后的独苗
return /** @type{ { push:typeof push } } */(mod);
};
module.exports = { getQue, newQue };
(() => { // test
[ // 多路之间,并行
{ key: 'que_1', tasks: '12345', delay: 1100 }, // 第一路,串行: 1->2->3->4->5
{ key: 'que_2', tasks: 'ABCDE', delay: 1300 }, // 第二路,串行:A->B->C->D->E
{ key: 'que_3', tasks: 'abcde', delay: 700 }, // 第三路,串行:a->b->c->d->e
].forEach(row => {
const cb = () => console.log(`done${'.'.repeat(16)}${row.key}`); // 某队列结束了
const queue = newQue(row.key, cb);
const { key, tasks, delay } = row;
tasks.split('').forEach(word => {
const param = `${key} - ${word} - ${delay}`;
const sleep = () => new Promise(res => setTimeout(res, delay, param));
const task = () => sleep().then(console.log); // 某任务结束了
queue.push(task); // 扔进队列,完事(爽歪歪)
})
});
})
() // 注释此行关闭测试
;
设计思路
这段代码实现了一个多队列并行、单队列串行的任务调度器。每个队列有三个特点:
- 自入:创建时自动注册到全局映射表
- 自驱:第一个任务推入时自动启动执行
- 自毁:队列清空后自动从映射表中删除
核心实现解析
1. 哨兵节点实现双向链表
javascript
ini
const sentinel = Object.create(null);
sentinel.next = sentinel;
sentinel.prev = sentinel;
这是实现链表的一种经典技巧。哨兵节点永远存在,自身代表"空",它的 next 指向链表头,prev 指向链表尾。
这样设计的好处:
- 空队列判断简单 :
sentinel.next === sentinel - 插入删除操作统一:无需特殊处理边界情况
- 代码简洁:所有操作都围绕哨兵节点进行
2. 自驱机制
javascript
scss
if (sentinel.next === sentinel) {
Promise.resolve().then(run)
}
当推入第一个任务时,链表从空变为非空,此时通过微任务启动执行器 run。这种设计确保了:
- 队列空闲时不会占用资源
- 有任务时自动开始处理
- 使用微任务避免同步调用可能带来的栈溢出
3. 异步递归实现串行
javascript
scss
const run = () => {
const task = peek();
if (!task) {
del();
if (cb) { cb() }
return;
}
task()
.finally(shift)
.finally(run);
};
这是整个队列的核心逻辑:
peek()获取头节点任务但不删除- 执行任务,无论成功失败都执行
finally shift()删除已执行的任务节点run()再次调用自身,处理下一个任务
这种递归是异步的,不会造成调用栈溢出,因为每个任务执行完后才调用下一个。
4. 自毁机制
javascript
scss
if (!task) {
del(); // 从映射表删除
if (cb) { cb() } // 回调通知外部
return;
}
当队列处理完所有任务后,自动从全局映射表中删除,释放资源。回调函数让外部能够感知队列的完成状态。
使用示例
javascript
javascript
// 创建三个队列,并行执行
[
{ key: 'que_1', tasks: '12345', delay: 1100 },
{ key: 'que_2', tasks: 'ABCDE', delay: 1300 },
{ key: 'que_3', tasks: 'abcde', delay: 700 },
].forEach(row => {
// 创建队列,指定完成回调
const queue = newQue(row.key, () => {
console.log(`队列 ${row.key} 执行完毕`);
});
// 推送任务
row.tasks.split('').forEach(word => {
const task = () => new Promise(resolve => {
setTimeout(() => {
console.log(`${row.key} - ${word}`);
resolve();
}, row.delay);
});
queue.push(task);
});
});
// 输出示例(各队列任务交错输出):
// que_3 - a
// que_3 - b
// que_1 - 1
// que_3 - c
// que_2 - A
// que_3 - d
// que_1 - 2
// ...
// 队列 que_3 执行完毕
// 队列 que_1 执行完毕
// 队列 que_2 执行完毕
类型说明
代码中使用了 JSDoc 进行类型标注:
javascript
php
/** @typedef { ReturnType<typeof newQue> } que */ // 队列类型
/** @typedef{ () => Promise<any> } task */ // 任务类型
/** @typedef{ string | number } key */ // 键类型
/** @typedef{ (...rest:any)=>void } callback */ // 回调类型
这些类型标注配合 VSCode 可以获得 TypeScript 级别的智能提示。
应用场景
- 请求队列:多个 API 端点,每个端点的请求需要串行处理
- 文件上传:避免同时上传太多文件导致连接数超限
- 数据库操作:确保对同一资源的操作按顺序执行
- 消息处理:保证同一会话的消息顺序
- 具体应用:levelDB的,按key 串行访问
总结
这段代码的精髓在于:
- 数据结构层面:用哨兵节点简化链表操作
- 控制流层面:用异步递归实现串行执行
- 生命周期层面:实现自入、自驱、自毁的智能管理
代码虽短,但包含了数据结构、异步编程、资源管理等多个方面的考量。