在javaScript项目开发中,异步编程是不可或缺的一部分。从网络请求到延时操作,异步操作使得我们能够在等待某个任务完成时继续执行其他任务,提高应用的响应性和性能。然而,随着应用逻辑的复杂化,管理这些异步任务的难度也随之增加。如何有效地组织和控制这些异步任务,成为了开发高效、可维护应用的关键。本文使用JavaScript实现一个异步队列来优雅地管理复杂的异步任务流。
异步编程的挑战
在深入异步队列的实现之前,让我们先回顾一下在JavaScript异步编程中常见的几个挑战:
- 回调地狱:过度使用回调函数可能导致代码难以阅读和维护,尤其是当你有多个需要顺序执行的异步操作时。
- 并发控制:同时执行多个异步操作时,如何有效地管理它们的完成状态并处理它们的结果。
- 错误处理:在异步操作链中适当地捕获和处理错误。
为了解决这些问题,许多开发者转向了Promise
和async/await
语法。虽然这些特性极大地改善了异步编程的体验,但在某些场景下,我们仍然需要更细粒度的控制,尤其是当我们需要按顺序执行一系列复杂的异步任务,或者需要在任务间传递数据时。这正是异步队列派上用场的时候。
异步队列(AsyncQueue
)的概念
异步队列是一种数据结构,它按照特定的顺序执行异步任务,每个任务在前一个任务完成后开始。这种模式对于需要严格顺序执行的异步操作非常有用,如连续的网络请求,其中后一个请求依赖于前一个请求的结果。
AsyncQueue
类设计
在AsyncQueue
的设计中,我们将关注以下几个关键部分:
- 任务的存储和管理:队列需要按顺序存储即将执行的异步任务。
- 任务执行的控制:提供方法来开始执行队列中的任务,并在当前任务完成后自动执行下一个任务。
- 动态任务管理:允许在队列执行过程中动态添加或移除任务。
实现AsyncQueue
接下来,我们按照先前的概述来具体实现AsyncQueue
类。这里用TypeScript实现。
步骤 1: 定义基础结构
首先,我们定义了任务(AsyncTask
)的结构,以及异步任务回调(AsyncCallback
)的类型。
typescript
export type NextFunction = (nextArgs?: any) => void;
export type AsyncCallback = (
next: NextFunction,
params: any,
args: any
) => void;
interface AsyncTask {
/**
* 任务uuid
*/
uuid: number;
/**
* 任务开始执行的回调
* params: push时传入的参数
* args: 上个任务传来的参数
*/
callbacks: Array<AsyncCallback>;
/**
* 任务参数
*/
params: any;
}
NextFunction
是一个函数,当当前任务完成时,它会被调用以触发队列中的下一个任务。AsyncCallback
是任务的实际执行函数,它接收next
函数、任务参数params
以及上一个任务的结果args
。AsyncTask
接口定义了任务的结构,包括唯一标识符uuid
、回调函数数组callbacks
和任务参数params
。
步骤 2: 实现AsyncQueue
类
现在,开始实现AsyncQueue
类,它包含了任务队列的核心逻辑。
typescript
export class AsyncQueue {
private _runningAsyncTask: AsyncTask | null = null;
private static _$uuid_count: number = 1;
private _queues: Array<AsyncTask> = [];
public get queues (): Array<AsyncTask> {
return this._queues;
}
private _isProcessingTaskUUID: number = 0;
private _enable: boolean = true;
/**
* 任务队列完成回调
*/
public complete: Function | null = null;
constructor() {}
// 方法的具体实现将在后面提供
}
在AsyncQueue
中,我们使用了以下几个关键属性:
_runningAsyncTask
: 当前正在执行的任务。_$uuid_count
: 用于生成任务的唯一标识符。_queues
: 存储待执行任务的队列。_enable
: 控制队列是否可以执行任务。complete
: 任务队列完成回调
步骤 3: 添加任务到队列
使用push
方法向队列中添加单个任务,使用pushMulti
添加多个需要并发执行的任务:
typescript
/**
* push一个异步任务到队列中
* 返回任务uuid
*/
public push (callback: AsyncCallback, params: any = null): number {
const uuid = AsyncQueue._$uuid_count++;
this._queues.push({
uuid: uuid,
callbacks: [callback],
params: params
});
return uuid;
}
/**
* push多个任务,多个任务函数会同时执行,
* 返回任务uuid
*/
public pushMulti (params: any, ...callbacks: AsyncCallback[]): number {
const uuid = AsyncQueue._$uuid_count++;
this._queues.push({
uuid: uuid,
callbacks: callbacks,
params: params
});
return uuid;
}
push
和pushMulti
允许动态地向队列中添加任务,无论是单个任务还是多个任务同时执行。
步骤 4: 移除任务和清空队列
typescript
/** 移除一个还未执行的异步任务 */
public remove (uuid: number) {
if (this._runningAsyncTask?.uuid === uuid) {
console.warn("A running task cannot be removed");
return;
}
for (let i = 0; i < this._queues.length; i++) {
if (this._queues[i].uuid === uuid) {
this._queues.splice(i, 1);
break;
}
}
}
/**
* 清空队列
*/
public clear () {
this._queues = [];
this._isProcessingTaskUUID = 0;
this._runningAsyncTask = null;
}
/**
* 是否有正在处理的任务
*/
public get isProcessing (): boolean {
return this._isProcessingTaskUUID > 0;
}
remove
方法允许从队列中移除尚未执行的任务,clear
方法用于清空整个队列:
步骤 5: 控制队列执行
play
方法用于从队列中取出任务并执行。对于单个任务,我们直接调用其回调函数;对于并发任务,我们同时调用它们的回调函数,并等待它们全部完成后才继续, 若队列的大小为0,则代表异步任务队列执行完毕,触发任务队列完成回调complete
。
typescript
/**
* 开始运行队列
*/
public play (args: any = null) {
if (this.isProcessing) {
return;
}
if (!this._enable) {
return;
}
const actionData: AsyncTask = this._queues.shift()!;
if (actionData) {
this._runningAsyncTask = actionData;
const taskUUID: number = actionData.uuid;
this._isProcessingTaskUUID = taskUUID;
const callbacks: Array<AsyncCallback> = actionData.callbacks;
if (callbacks.length === 1) {
const nextFunc: NextFunction = (nextArgs: any = null) => {
this.next(taskUUID, nextArgs);
};
callbacks[0](nextFunc, actionData.params, args);
} else {
// 多个任务函数同时执行
let fnum: number = callbacks.length;
const nextArgsArr: any[] = [];
const nextFunc: NextFunction = (nextArgs: any = null) => {
--fnum;
nextArgsArr.push(nextArgs || null);
if (fnum === 0) {
this.next(taskUUID, nextArgsArr);
}
};
const knum = fnum;
for (let i = 0; i < knum; i++) {
callbacks[i](nextFunc, actionData.params, args);
}
}
} else {
this._isProcessingTaskUUID = 0;
this._runningAsyncTask = null;
// console.log("任务完成")
if (this.complete) {
this.complete(args);
}
}
}
在任务执行完成后,需要一种方式来继续执行队列中的下一个任务。这是通过next
方法实现的,该方法将根据当前任务的uuid
来确定是否继续:
typescript
protected next(taskUUID: number, args: any = null) {
if (this._isProcessingTaskUUID === taskUUID) {
this._isProcessingTaskUUID = 0;
this._runningAsyncTask = null;
this.play(args);
}
}
完整的AsyncQueue
使用示例
通过AsyncQueue
,我们可以很好地管理复杂的异步任务流程。以下是一个使用AsyncQueue
的示例:
typescript
const queue = new AsyncQueue();
queue.push((next, params) => {
console.log("执行任务 1");
// 模拟异步操作
setTimeout(() => {
console.log("任务 1 完成");
next();
}, 1000);
});
queue.push((next, params) => {
console.log("执行任务 2");
setTimeout(() => {
console.log("任务 2 完成");
next();
}, 1000);
});
queue.complete = () => console.log("所有任务执行完毕");
queue.play();
这个简单的例子展示了如何使用AsyncQueue
顺序执行两个异步任务,并在所有任务完成后打印一条消息:
shell
执行任务 1
任务 1 完成
执行任务 2
任务 2 完成
所有任务执行完毕
至此,实现了一个简单的异步队列任务管理系统AsyncQueue
, 它提供了一种高效、灵活的方式来管理和控制异步任务的执行。通过将异步任务封装成队列,可以确保它们按预期的顺序执行,同时保持代码的清晰和可维护性。这种模式特别适用于处理复杂的业务逻辑,如顺序执行网络请求或依赖于前一个任务结果的操作。