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

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

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 串行访问

总结

这段代码的精髓在于:

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

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

相关推荐
ivwsjc8 小时前
vue3 echarts地图点到点之间的飞线图
前端·javascript·vue·echarts
小李子呢02118 小时前
JS中的Set 核心认知
前端·javascript·es6
程序员阿耶8 小时前
【前端面试知识点】CSS contain 属性如何用于性能优化?它有哪些可选值及作用?
前端·面试
阳火锅8 小时前
34岁前端倒计时:老板用AI手搓系统那天,我知道我的“体面退休”是个笑话
前端·后端·程序员
姓王者8 小时前
# 解决 Nautilus 自定义终端插件安装依赖问题
前端·后端·全栈
宸翰8 小时前
在VS code中如何舒适的开发Python
前端·python
奋斗的小鱼干8 小时前
windows龙虾的安装
前端
程序员阿峰9 小时前
【JavaScript面试题-this 绑定】请说明 `this` 在不同场景下的指向(默认、隐式、显式、new、箭头函数)。
前端·javascript·面试
用户318730828659 小时前
Python 短信接口高效集成指南:Django/Flask 框架最佳实践
前端
刘宇琪9 小时前
配置 TypeScript 支持 Vite 中的路径别名和自动类型提示
前端