用TypeScript实现高效的异步队列任务管理

在javaScript项目开发中,异步编程是不可或缺的一部分。从网络请求到延时操作,异步操作使得我们能够在等待某个任务完成时继续执行其他任务,提高应用的响应性和性能。然而,随着应用逻辑的复杂化,管理这些异步任务的难度也随之增加。如何有效地组织和控制这些异步任务,成为了开发高效、可维护应用的关键。本文使用JavaScript实现一个异步队列来优雅地管理复杂的异步任务流。

异步编程的挑战

在深入异步队列的实现之前,让我们先回顾一下在JavaScript异步编程中常见的几个挑战:

  1. 回调地狱:过度使用回调函数可能导致代码难以阅读和维护,尤其是当你有多个需要顺序执行的异步操作时。
  2. 并发控制:同时执行多个异步操作时,如何有效地管理它们的完成状态并处理它们的结果。
  3. 错误处理:在异步操作链中适当地捕获和处理错误。

为了解决这些问题,许多开发者转向了Promiseasync/await语法。虽然这些特性极大地改善了异步编程的体验,但在某些场景下,我们仍然需要更细粒度的控制,尤其是当我们需要按顺序执行一系列复杂的异步任务,或者需要在任务间传递数据时。这正是异步队列派上用场的时候。

异步队列(AsyncQueue)的概念

异步队列是一种数据结构,它按照特定的顺序执行异步任务,每个任务在前一个任务完成后开始。这种模式对于需要严格顺序执行的异步操作非常有用,如连续的网络请求,其中后一个请求依赖于前一个请求的结果。

AsyncQueue类设计

AsyncQueue的设计中,我们将关注以下几个关键部分:

  1. 任务的存储和管理:队列需要按顺序存储即将执行的异步任务。
  2. 任务执行的控制:提供方法来开始执行队列中的任务,并在当前任务完成后自动执行下一个任务。
  3. 动态任务管理:允许在队列执行过程中动态添加或移除任务。

实现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;
  }
  • pushpushMulti允许动态地向队列中添加任务,无论是单个任务还是多个任务同时执行。

步骤 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, 它提供了一种高效、灵活的方式来管理和控制异步任务的执行。通过将异步任务封装成队列,可以确保它们按预期的顺序执行,同时保持代码的清晰和可维护性。这种模式特别适用于处理复杂的业务逻辑,如顺序执行网络请求或依赖于前一个任务结果的操作。

相关推荐
昨晚我输给了一辆AE863 小时前
为什么现在不推荐使用 React.FC 了?
前端·react.js·typescript
Wect10 小时前
LeetCode 130. 被围绕的区域:两种解法详解(BFS/DFS)
前端·算法·typescript
Dilettante25810 小时前
这一招让 Node 后端服务启动速度提升 75%!
typescript·node.js
jonjia1 天前
模块、脚本与声明文件
typescript
jonjia1 天前
配置 TypeScript
typescript
jonjia1 天前
TypeScript 工具函数开发
typescript
jonjia1 天前
注解与断言
typescript
jonjia1 天前
IDE 超能力
typescript
jonjia1 天前
对象类型
typescript
jonjia1 天前
快速搭建 TypeScript 开发环境
typescript