哨兵节点实现的自驱式任务队列

哨兵节点实现的自驱式任务队列,不废话,上码:

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. 自入:创建时自动注册到全局映射表
  2. 自驱:第一个任务推入时自动启动执行
  3. 自毁:队列清空后自动从映射表中删除

核心实现解析

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 级别的智能提示。

应用场景

  1. 请求队列:多个 API 端点,每个端点的请求需要串行处理
  2. 文件上传:避免同时上传太多文件导致连接数超限
  3. 数据库操作:确保对同一资源的操作按顺序执行
  4. 消息处理:保证同一会话的消息顺序
  5. 具体应用:levelDB的,按key 串行访问

总结

这段代码的精髓在于:

  • 数据结构层面:用哨兵节点简化链表操作
  • 控制流层面:用异步递归实现串行执行
  • 生命周期层面:实现自入、自驱、自毁的智能管理

代码虽短,但包含了数据结构、异步编程、资源管理等多个方面的考量。

相关推荐
阿星AI工作室1 小时前
我做了个飞书转公众号排版器,6套高颜值主题想换就换
前端·人工智能
_Eleven2 小时前
继TailWindCss和UnoCss后的CSS-in-JS vs Utility-First 深度对比
前端
GinoWi2 小时前
CSS属性 - 边距属性
前端
豆苗学前端2 小时前
彻底讲透医院移动端手持设备PDA离线同步架构:从"记账本"到"分布式共识",吊打面试官
前端·javascript·后端
AKclown2 小时前
Vibe coding(AI编程一网打尽)
前端·react.js
埋塘小王子2 小时前
React项目白屏兜底神器?ErrorBounary你了解吗?
前端
却尘2 小时前
一个 ERR_SSL_PROTOCOL_ERROR 让我们排查了三层问题,最后发现根本不是 SSL 的锅
前端·后端·网络协议
用户83040713057012 小时前
如何处理axios请求中post请求的坑
前端
行走在顶尖2 小时前
vue3项目搭建基础
前端