JavaScript + TypeScript 实现并发队列

1. 前言

本文使用了 TypeScript 和 JavaScript,可能有的读者并没有学过 TypeScript,担心看不懂。其实我认为有了 TypeScript 你应该更容易看懂,因为 TypeScript 仅仅是繁琐了一点,因为它只是给变量加上了类型,但是它能增加代码的可读性和可维护性,所以你应该能快速理解。

安装 TypeScript 见文末。

生活中许多同时发生的事情,比如:你在打代码,他在打代码,她也在打代码,而我在看你们打代码。这不是并发而是并行。

并发和并行的最大区别就是多件事情是交给了一个人做还是多个人做。如果是交给了一个人做就是并发,交给了多个人做就是并行。而这里要说的是并发执行,并使用 TypeScript 和 JavaScript 来实现一个并发队列。

在生活中我们能处处看到并发队列,与本文要说的并发队列非常像。比如说排队,在一个售票窗口,只能一个一个的进行,后面的人只能先等待前面的人买完票了,处理完手续后才能进行买票。本文要讲的并发队列原理与这个非常像。

2. 核心代码解析

先不展示全部的代码,讲清楚核心的逻辑后,其他的代码也就是起个辅助的作用,也就没有难理解的地方了。

核心代码我将其分为以下几个部分,从易到难进行讲解:

  • 使用示例
  • 添加任务
  • 运行任务
  • 执行一个任务
  • 判断是否执行结束

2.1. 使用示例

可以看到下面定义并添加了了两个任务,均在两秒后输出一段话到控制台,但是我们在创建并发队列时指定最大并发数为 1,所以一次只能执行一个任务,并且该任务队列的执行顺序是先添加的先执行。

ts 复制代码
// 所有的任务执行完毕后的回调函数
let callback = (result: any) => {
    console.log(result);
};

let concurrencyTask = new ConcurrencyTask(1, callback);

// 添加任务
concurrencyTask.addTask((resolve, reject) => {
    setTimeout(() => {
        console.log("2 秒后得到执行"); // 2 秒后输出
        resolve();
    }, 2000);
});

concurrencyTask.addTask((resolve, reject) => {
    setTimeout(() => {
        console.log("4 秒后得到执行"); // 4 秒后输出
        resolve();
    }, 2000);
});

concurrencyTask.run(false);

2.2. 添加任务

下面是添加任务的代码,添加的任务要求是一个函数,并在执行时会接收到三个参数:resolverejecctargs。这三个参数分别为 Promise 的 resolvereject,而 args 是函数执行需要的可变参数,如果在任务队列执行过程中添加任务则不允许加入。

ts 复制代码
type Task = (resolve: Function, reject: Function, ...args: Array<any>) => any;

/**
 * 添加任务到任务队列, 不会执行
 * @param task 任务
 * @return 是否添加成功, 如果任务处于执行阶段返回 false
 */
public addTask(task: Task): boolean {
    if (!this.getRunning()) {
        this.taskList.push(task);
        return true;
    }
    return false;
}

2.3. 运行任务

canAbort 参数表示队列执行过程中是否可中断,在执行的任务中调用 reject 函数即可中断任务的执行,中断后任务队列将进行重置,清空已执行的和未执行的任务以及重置其他数据。

下面的代码的意思是执行指定最大并发数的数量的任务,如果最大并发数大于任务总数量,则以任务总数量为最大并发数来执行。

ts 复制代码
/**
 * 开始运行任务
 * @param canAbort 是否可中断
 * @param args 任务执行参数
 */
public run(canAbort: boolean = false, ...args: Array<any>): void {
    this.canAbort = canAbort;
    this.setRunning(true);
    let length = this.taskList.length;
    let maxConcurrency = Math.min(this.getMaxConcurrency(), length);
    for (let index = 0; index < maxConcurrency; index++) {
        this.executeSingleTask(args);
    }
}

2.4. 执行一个任务

由于任务执行具有异步性,所以我们使用 Promise 来包裹任务,并把 resolve, reject 传递给任务函数,让它来决定任务何时结束。

当一个任务调用了 resolve 函数时,将会判断任务是否全部得到执行,即执行 judgeExecuteEnd 函数,如果任务调用 reject 函数,将会判断是否可以中断任务的执行,并重置任务队列,当然不想重置任务队列可以在源代码上进行修改,这里我就不改了。

然后每个任务的 promise 会保存在 taskPromiseList 变量中,它是一个 Promise 类型的数组。

ts 复制代码
/**
 * 执行单个任务
 * @param args 函数执行参数
 */
private executeSingleTask(...args: Array<any>): void {
    let promise = new Promise<void>((resolve, reject) => {
        let result = this.taskList[this.taskIndex++](resolve, reject, args);
        this.handleResult.push(result);
    });
    promise.then(() => {
        this.judgeExecuteEnd(args);
    }).catch((error) => {
        // 如果可以中断任务的执行, 则重置任务队列
        if (this.canAbort) {
            this.reset();
            return;
        }
        console.error(error);
    });
    this.taskPromiseList.push(promise);
}

2.5. 判断是否执行结束

下面的代码中 taskIndex 是当前任务的索引,runOver 为是否执行结束的标志。

这里我们判断 taskPromiseList 中的 promise 是否全部完成

ts 复制代码
/**
 * 判断是否执行结束
 * @param args 函数执行所需参数
 */
private judgeExecuteEnd(args: Array<any>): void {
    // 如果全部任务都得到执行, 并且执行没有结束
    // 设置 runOver 的原因是最后几个并发执行的任务在执行完毕后都会
    // 触发该函数, 而 runOverCallback 函数应只执行一次
    if (this.taskIndex >= this.taskList.length && !this.runOver) {
        this.runOver = true;
        let result = this.handleResult;
        Promise.all(this.taskPromiseList).then(() => {
            this.runOverCallback && this.runOverCallback(result);
        }).catch((error) => {
            // 如果不允许中断,则会执行任务全部完成回调
            if(!this.canAbort) {
                this.runOverCallback && this.runOverCallback(result);
            }
            console.error(error);
        });
        this.reset();
        return;
    }
    // 如果没有执行结束,就执行下一个任务
    this.executeSingleTask(args);
}

3. 源代码展示

下面的代码直接复制到 ts 文件中是不会有任何的效果的,因为浏览器不能解析 ts 代码,我们需要使用 ts 编译器将其编译为 js 代码后,再引用 js 文件即可。安装 TypeScript 见文末。

ts 复制代码
/* 
  功能描述: 并发队列
  创建时间: 2023年 12月 17日
 */

type Task = (resolve: Function, reject: Function, ...args: Array<any>) => any;
type ResultCallback = (result: Array<any>) => any;

/**
 * 并发任务队列
 */
class ConcurrencyTask {

    /**
     * 任务集合
     */
    private taskList: Array<Task>;

    /**
     * 处理结果
     */
    private handleResult: Array<any>;

    /**
     * 是否正在执行任务
     */
    private running: boolean;

    /**
     * 最大并发数
     */
    private maxConcurrency: number;

    /**
     * 默认的最大并发数
     */
    private static DEFAULT_MAX_CONCURRENCY: number = 2;

    /**
     * 当前任务索引
     */
    private taskIndex: number;

    /**
     * 用 promise 包裹任务
     */
    private taskPromiseList: Array<Promise<void>>;

    /**
     * 是否可中断
     */
    private canAbort: boolean;

    /**
     * 执行结束
     */
    private runOver: boolean;

    /**
     * 任务全部执行完毕时的回调函数
     */
    private runOverCallback: ResultCallback;

    /**
     * 创建并发任务队列
     * @param maxConcurrency 最大并发数
     * @param runOverCallback 任务全部执行完毕后的回调
     */
    public constructor(maxConcurrency: number = ConcurrencyTask.DEFAULT_MAX_CONCURRENCY, runOverCallback: ResultCallback) {
        this.setRunOverCallback(runOverCallback);
        this.setMaxConcurrency(maxConcurrency);
        this.initial();
    }

    private initial(): void {
        this.canAbort = false;
        this.reset();
    }

    /**
     * 添加任务到任务队列, 不会执行
     * @param task 任务
     * @return 是否添加成功, 如果任务处于执行阶段返回 false
     */
    public addTask(task: Task): boolean {
        if (!this.getRunning()) {
            this.taskList.push(task);
            return true;
        }
        return false;
    }

    /**
     * 开始运行任务
     * @param canAbort 是否可中断
     * @param args 任务执行参数
     */
    public run(canAbort: boolean = false, ...args: Array<any>): void {
        this.canAbort = canAbort;
        this.setRunning(true);
        let length = this.taskList.length;
        let maxConcurrency = Math.min(this.getMaxConcurrency(), length);
        for (let index = 0; index < maxConcurrency; index++) {
            this.executeSingleTask(args);
        }
    }

    /**
     * 执行单个任务
     * @param args 函数执行参数
     */
    private executeSingleTask(...args: Array<any>): void {
        let promise = new Promise<void>((resolve, reject) => {
            let result = this.taskList[this.taskIndex++](resolve, reject, args);
            this.handleResult.push(result);
        });
        promise.then(() => {
            this.judgeExecuteEnd(args);
        }).catch((error) => {
            // 如果可以中断任务的执行, 则重置任务队列
            if (this.canAbort) {
                this.reset();
                return;
            }
            console.error(error);
        });
        this.taskPromiseList.push(promise);
    }

    /**
     * 判断是否执行结束
     * @param args 函数执行所需参数
     */
    private judgeExecuteEnd(args: Array<any>): void {
        // 如果全部任务都得到执行, 并且执行没有结束
        // 设置 runOver 的原因是最后几个并发执行的任务在执行完毕后都会
        // 触发该函数, 而 runOverCallback 函数应只执行一次
        if (this.taskIndex >= this.taskList.length && !this.runOver) {
            this.runOver = true;
            let result = this.handleResult;
            Promise.all(this.taskPromiseList).then(() => {
                this.runOverCallback && this.runOverCallback(result);
            }).catch((error) => {
                if(!this.canAbort) {
                    this.runOverCallback && this.runOverCallback(result);
                }
                console.error(error);
            });
            this.reset();
            return;
        }
        this.executeSingleTask(args);
    }

    private reset(): void {
        this.taskList = [];
        this.taskIndex = 0;
        this.taskPromiseList = [];
        this.running = false;
        this.handleResult = [];
    }

    private setRunning(running: boolean): void {
        this.running = running;
    }

    public getRunning(): boolean {
        return this.running;
    }

    /**
     * 设置任务全部执行完毕后的回调函数, 如果队列正在执行则返回 false
     * @param runOverCallback 回调函数
     */
    public setRunOverCallback(runOverCallback: ResultCallback): boolean {
        if(!this.getRunning()) {
            this.runOverCallback = runOverCallback;
            return true;
        }
        return false;
    }

    /**
     * 设置最大并发数, 如果正在执行返回 false
     * @param maxConcurrency 最大并发数, 小于等于 0 时使用默认值
     */
    public setMaxConcurrency(maxConcurrency: number): boolean {
        if(maxConcurrency <= 0) {
            this.maxConcurrency = ConcurrencyTask.DEFAULT_MAX_CONCURRENCY;
        }
        if (!this.getRunning()) {
            this.maxConcurrency = maxConcurrency;
            return true;
        }
        return false;
    }

    public getMaxConcurrency(): number {
        return this.maxConcurrency;
    }
}

4. 安装 TypeScript

由于 TypeScript 是运行在 Node.js 上的,所以我们还需要安装 Node.js,安装 Node.js 可前往 Node.Js 中文网

这里仅提供 windows 上的 TypeScript 的安装方式。

首先以管理员的方式进入 cmd(win + R,输入 cmd,然后 ctrl + shift + enter 即可)。

使用以下的命令全局安装:

shell 复制代码
npm i -g typescript

之后在任意目录下创建一个 ts 文件,然后在该文件夹下打开 cmd,执行 tsc xx.ts 就会得到一个编译后的 ja 文件。

相关推荐
m0_7482552636 分钟前
前端安全——敏感信息泄露
前端·安全
鑫~阳2 小时前
html + css 淘宝网实战
前端·css·html
Catherinemin2 小时前
CSS|14 z-index
前端·css
2401_882727574 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
NoneCoder4 小时前
CSS系列(36)-- Containment详解
前端·css
anyup_前端梦工厂4 小时前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand4 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL4 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
六卿4 小时前
react防止页面崩溃
前端·react.js·前端框架
z千鑫5 小时前
【前端】详解前端三大主流框架:React、Vue与Angular的比较与选择
前端·vue.js·react.js