likes
comments
collection
share

队列在前端中的使用

作者站长头像
站长
· 阅读数 43

队列是一种常见的数据结构,可以看做一种特殊的列表。数据的插入与删除操作仅限于两端。

队列与栈类似,不同点在于

  • 栈:后进先出
  • 队列:先进先出

而在我们生活中常见的例子便是排队,比如我们排队上公交,排在前面的先上车,排在后面的后上,新来的人不允许插队,只能在队尾排队等待。

队列在前端中的使用

图1. 队列示意图

代码实现

队列常用的操作有:出队与入队。出队只能从队首操作,入队仅能从队尾操作。

数组由于可以用 shift 删除第一个元素,push 在数组中添加新的元素,与队列的操作非常吻合,因此可作为队列的最佳实践。

class Queue {
    items = []

    constructor() {}

    enqueue(element) {
        this.items.push(element);
    }
    dequeue() {
        this.items.shift();
    }
    peek() {
        return this.items[0];
    }
    size() {
        return this.items.length;
    }
    isEmpty() {
        return this.size() === 0;
    }
}

Javascript也可以使用其他数据类型来实现队列,如:Object,ES 的 Set、Map 等。但是没有数组这么简单,感兴趣的可自行搜索。

任务队列

我们使用队列最终都是为了完成某项特定的任务,因此需要在原有的基础上实现任务队列。

而任务队列通常有流量限制,并且都是异步的,比如我们去银行排队办理个人业务,银行有 3 个个人业务的窗口,那每次最多允许 3 个人同时办理业务。而窗口处理各业务的完成时间是不定的。有的窗口快,有的慢,这就是异步了。

如果我们将3个窗口,看成每次只允许运行1个任务的3个小队列,从等待队列中取最前面的一个人来加入这个小队列,就过于僵化,把问题想的复杂了。

不如仅做一个等待队列,先判断当前是否可以开始办理业务,如果是,把等待队列中的人拉出来开始办理即可。

队列在前端中的使用

图2. 等待队列与3个执行队列、单个等待队列对比

这样反而简单易操作,代码如下:

class TaskQueue {
    concurrency = 1;
    tasks = [];
    runningTasksCount = 0;
    constructor(concurrency) {
        this.concurrency = concurrency;
    }
    pushTask(task) {
        this.tasks.push(task);
        this.runNextTask();
    }
    pushTasks(tasks) {
        this.tasks.push(...tasks);
        this.runNextTask();
    }
    runNextTask() {
        if (this.runningTaskCount >= this.concurrency || this.tasks.length === 0) {
            return;
        }
        const task = this.tasks.shift();
        if (task) {
            this.runningTaskCount ++;
            try {
                task();
            } catch (e) {
                console.log(e);
            } finally {
                this.runningTaskCount --;
                this.runNextTask();
            }
        }
    }
}

此时,入队 enqueue 为了支持多任务,改名 pushTask 和 pushTasks。出队 dequeue 移入了 runNextTask 中。虽然表面上变成了数组,但是我们清楚其数据结构是队列,必须遵循先进先出得,不做 unshift、pop 等破坏数组操作。

这样我们就得到了一个任务队列。

异步任务处理

异步任务难免要做一些业务处理,这时候如果把异步任务的逻辑放进此公共类里就不合适了。

因此可以通过继承、重载 runNextTask 方法就可以实现自己想要的处理了。如果你还想返回一些任务统计数据,则可以通过发布订阅模式进行事件分发。

class RequestTask extends TaskQueue {
    runNextTask() {
        if (this.runningTaskCount >= this.concurrency || this.tasks.length === 0) {
            return;
        }
        const task = this.tasks.shift();
        if (task) {
            this.runningTaskCount ++;
            task().then(data => {
                console.log(data); // 对所有数据做相同处理
            }).catch((e) => {
                console.log(e);
            }).finally(() => {
                event.emit('task-finished', this.size()); // 返回剩余任务数
                this.runningTaskCount --;
                this.runNextTask();
            });
        }
    }
}

这样一个异步处理的任务就完成了。

使用示例

// 1. 创建任务
const rq = new RequestTask(5);

// 2. 发起请求
rq.pushTasks([() => {
   // new Promise() 可以替换成 axios 或其他请求方法
   return (new Promise(...)).then((data) => {
     // 有自行处理的 then,需要最后回传数据给事件中心。
     return data;
   });
},...]);

// 3. 订阅事件
event.on('task-finished', (data) => {
    console.log(data); // 剩余任务数
});

上面的代码经过修改,就可以在以下的地方使用了:

  • 大文件切片上传
  • 多次请求后端接口,返回数据集中处理。

可以看到的是,我们通过发布订阅模式,把任务队列和业务代码解耦出来了。常常听说,前端不知道设计模式怎么用,其实只要在抽象、解耦上多想想,这些知识是随时可以用上的。

扩展

概念理清

没有异步队列这个概念!如果有,这个队列违背了“先进先出”的原则。

继承的缺点

继承的方式有个被人诟病的地方是,如果有过多的层级,后期维护难度会级数上升。因此不建议超过三层的继承。

除了继承外,如果使用 typescript 也可以通过接口实现,减少上述不利维护的因素。

类图:

继承类
实现接口
TaskQueue
tasks: []
concurrency: number
runningTaskCount: number
pushTask()
pushTasks()
runNextTask()
RequestTask
runNextTask()
«interface»
ITaskQueue
tasks: []
concurrency: number
runningTaskCount: number
pushTask()
pushTasks()
runNextTask()
RequestTask2
tasks: []
concurrency: number
runningTaskCount: number
pushTask()
pushTasks()
runNextTask()

其他队列

除了异步任务外,还有其他一些队列形式,也可以参考标准的队列进行实现。

  • 动画队列:按顺序加载动画,让动画更流畅
  • 图片队列:预加载图片,减少堵塞

在这些代码中,你可能无法看到明确的 enqueue、dequeue,反而可能看到 Array.shift、Array.push 的操作,如果无其他破坏数组的操作,实际上所用的数据结构就是队列。

另外还有更复杂的循环队列、链队列等,可自行搜索了解。

总结

队列是一种常见的数据结构,其特点是先进先出,可以利用数组进行简单实现。

在前端中可用于多任务处理,常见于带并发控制的请求,如文件上传、按顺序请求等功能。这些都是任务队列的变形应用。

掌握好队列,能更好地做异步任务的解耦。

参考

chatGPT

转载自:https://juejin.cn/post/7215967109171003448
评论
请登录