Введение в очереди задач в TypeScript
Очередь задач (Queue) — одна из фундаментальных структур данных в программировании, работающая по принципу FIFO (First In, First Out — «первым пришёл — первым вышел»). Представьте обычную очередь в магазине: первый покупатель, который встал в очередь, обслуживается первым. В программировании очередь задач используется повсеместно: от обработки запросов на сервере до управления асинхронными операциями в браузере.
В этой статье мы разберём, как создавать и использовать очередь задач в TypeScript. Вы узнаете, как типизировать очередь, реализовать основные методы и применить её в реальных проектах. Материал рассчитан на начинающих разработчиков, знакомых с основами TypeScript.
Основные операции с очередью
Любая очередь задач должна поддерживать несколько базовых операций:
- enqueue (добавить элемент в конец очереди);
- dequeue (извлечь элемент из начала очереди);
- peek (посмотреть первый элемент без удаления);
- isEmpty (проверить, пуста ли очередь);
- size (узнать количество элементов).
В TypeScript мы можем реализовать очередь на основе массива или связного списка. Для простоты начнём с массива.
Реализация очереди на основе массива
Создадим класс Queue с использованием дженериков — это позволит хранить данные любого типа:
class Queue<T> {
private items: T[] = [];
// Добавить элемент в конец очереди
enqueue(element: T): void {
this.items.push(element);
}
// Удалить и вернуть первый элемент
dequeue(): T | undefined {
return this.items.shift();
}
// Посмотреть первый элемент без удаления
peek(): T | undefined {
return this.items[0];
}
// Проверить, пуста ли очередь
isEmpty(): boolean {
return this.items.length === 0;
}
// Получить размер очереди
size(): number {
return this.items.length;
}
}
Теперь можно создать очередь для любых типов данных:
// Очередь строк
const stringQueue = new Queue<string>();
stringQueue.enqueue('Задача 1');
stringQueue.enqueue('Задача 2');
console.log(stringQueue.dequeue()); // 'Задача 1'
// Очередь чисел
const numberQueue = new Queue<number>();
numberQueue.enqueue(10);
numberQueue.enqueue(20);
console.log(numberQueue.peek()); // 10
Недостатки реализации на массиве
Метод shift() в JavaScript/TypeScript имеет сложность O(n), так как после удаления первого элемента все остальные сдвигаются. Для больших объёмов данных это неэффективно. Решение — реализовать очередь на основе объекта или связного списка.
Оптимизированная очередь с объектом
Чтобы избежать сдвига элементов, будем хранить элементы в объекте и отслеживать индексы начала и конца:
class OptimizedQueue<T> {
private items: Record<number, T> = {};
private head: number = 0;
private tail: number = 0;
enqueue(element: T): void {
this.items[this.tail] = element;
this.tail++;
}
dequeue(): T | undefined {
if (this.isEmpty()) {
return undefined;
}
const item = this.items[this.head];
delete this.items[this.head];
this.head++;
return item;
}
peek(): T | undefined {
return this.items[this.head];
}
isEmpty(): boolean {
return this.tail === this.head;
}
size(): number {
return this.tail - this.head;
}
}
Теперь операции enqueue и dequeue выполняются за O(1) — константное время, независимо от размера очереди.
Практические примеры использования очередей
1. Очередь задач для асинхронной обработки
Представьте, что вам нужно последовательно обработать несколько запросов к API. Очередь гарантирует, что задачи будут выполняться в порядке поступления:
interface Task {
id: number;
name: string;
execute: () => Promise<void>;
}
class TaskQueue extends OptimizedQueue<Task> {
async processAll(): Promise<void> {
while (!this.isEmpty()) {
const task = this.dequeue();
if (task) {
console.log(`Выполняю задачу: ${task.name}`);
await task.execute();
console.log(`Задача ${task.name} завершена`);
}
}
}
}
// Использование
const queue = new TaskQueue();
queue.enqueue({
id: 1,
name: 'Загрузка данных',
execute: async () => {
// имитация запроса
await new Promise(resolve => setTimeout(resolve, 1000));
}
});
queue.enqueue({
id: 2,
name: 'Сохранение результата',
execute: async () => {
await new Promise(resolve => setTimeout(resolve, 500));
}
});
queue.processAll().then(() => console.log('Все задачи выполнены'));
2. Очередь с приоритетами (Priority Queue)
Иногда задачи нужно обрабатывать не строго в порядке поступления, а с учётом приоритета. Реализуем простую версию:
interface PriorityItem<T> {
value: T;
priority: number;
}
class PriorityQueue<T> {
private items: PriorityItem<T>[] = [];
enqueue(value: T, priority: number): void {
const newItem: PriorityItem<T> = { value, priority };
let added = false;
for (let i = 0; i < this.items.length; i++) {
if (this.items[i].priority > priority) {
this.items.splice(i, 0, newItem);
added = true;
break;
}
}
if (!added) {
this.items.push(newItem);
}
}
dequeue(): T | undefined {
return this.items.shift()?.value;
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
// Пример: задачи с разными приоритетами
const priorityQueue = new PriorityQueue<string>();
priorityQueue.enqueue('Низкий приоритет', 3);
priorityQueue.enqueue('Высокий приоритет', 1);
priorityQueue.enqueue('Средний приоритет', 2);
console.log(priorityQueue.dequeue()); // 'Высокий приоритет'
console.log(priorityQueue.dequeue()); // 'Средний приоритет'
3. Ограниченная очередь (Bounded Queue)
Иногда нужно ограничить максимальный размер очереди, чтобы избежать переполнения памяти:
class