实现一个 promise 构造器

111 阅读5分钟

实现一个 promise 构造器,要求具备以下功能

  • 支持重试次数,并且设定重试间隔
  • 支持超时控制
  • 支持优先级队列
  • 支持并发数限制
  • 支持暂停,恢复
  • 支持进度通知
  • 支持错误回调

用法如下,完整代码

const executor = new PromiseExecutor({
    maxRetries: 3,
    retryDelay: 1000,
    timeout: 5000,
    maxConcurrency: 2,
    onProgress: (completed, total) => {},
    onError: (failed, total) => {}
}); 
executor.addTask(async () => {/*...*/}, { priority: 1 });
executor.pause();
executor.resume();

v1 版本

需要实现的功能较多,而且其中有些功能存在逻辑关联,所以需要先实现一部分功能,然后在此基础上进行迭代,初步实现功能如下

1、支持并发数限制

2、支持暂停,恢复

3、支持进度通知,支持正确/错误回调

首先写出一个初步的的架构,addTask 用于添加任务,添加了暂停和恢复的函数

class PromiseExecutor {
  constructor(config) {
    this.config = config;
  }
  addTask() {}
  pause() {}
  resume() {}
}

1、支持并发数限制

需要新增一个数组(tasks)保存要执行的函数,新增一个变量(current)用于表示记录当前的并发量,还需要一个函数(run),专门用于执行函数,因为 addTask 只专注于添加任务

2、支持暂停,恢复

很简单,增加一个标志位进行判断即可(isRunning)

3、增加回调

增加对应的变量,然后在相关节点调用回调函数即可

class PromiseExecutor {
  constructor(config) {
    this.config = config;
    // 执行栈
    this.tasks = [];
    // 当前有几个任务在运行
    this.current = 0;
    // 是否正在运行,用来实现暂停
    this.isRunning = false;
    // 完成了几个任务
    this.completed = 0;
    // 成功了几个任务
    this.success = 0;
    // 失败了几个任务
    this.failed = 0;
    // 当前总共有多少任务
    this.total = 0;
  }
  async run() {
    // 若当前并发数小于最大并发量,且有待执行的任务,且没有被暂停,则开启循环
    while (
      this.current < this.config.current &&
      this.tasks.length &&
      this.isRunning
    ) {
      const { task } = this.tasks.shift();
      if (task) {
        try {
          // 修改并发数
          this.current++;
          // 使用 resolve 进行包裹,会继承 task 返回值的状态
          await Promise.resolve(task());
          this.success++;
          // 成功回调
          this.config.onSuccess(this.success, this.total);
        } catch (e) {
          // 失败回调
          this.config.onError(this.failed, this.total);
          console.log(e);
        } finally {
          // 修改并发数
          this.current--;
          this.completed++;
          // 进度回调
          this.config.onProgress(this.completed, this.total);
          // 启动下一次 run
          this.run();
        }
      }
    }
  }
  addTask(task) {
      this.total++;
      this.tasks.push(task)
      this.run()
  }
  pause() {
      this.isRunning = false
  }
  resume() {
      this.isRunning = false
      this.run()
  }
}

4、测试

可以看到,功能均正常

class PromiseExecutor {
  constructor(config) {
    this.config = config;
    // 执行栈
    this.tasks = [];
    // 当前有几个任务在运行
    this.current = 0;
    // 是否正在运行,用来实现暂停
    this.isRunning = false;
    // 完成了几个任务
    this.completed = 0;
    // 成功了几个任务
    this.success = 0;
    // 失败了几个任务
    this.failed = 0;
    // 当前总共有多少任务
    this.total = 0;
  }
  async run() {
    // 若当前并发数小于最大并发量,且有待执行的任务,且没有被暂停,则开启循环
    while (
      this.current < this.config.current &&
      this.tasks.length &&
      this.isRunning
    ) {
      const task = this.tasks.shift();
      if (task) {
        try {
          // 修改并发数
          this.current++;
          // 使用 resolve 进行包裹,会继承 task 返回值的状态
          await Promise.resolve(task());
          this.success++;
          // 成功回调
          this.config.onSuccess(this.success, this.total);
        } catch (e) {
          this.failed++
          // 失败回调
          this.config.onError(this.failed, this.total);
        } finally {
          // 修改并发数
          this.current--;
          this.completed++;
          // 进度回调
          this.config.onProgress(this.completed, this.total);
          // 启动下一次 run
          this.run();
        }
      }
    }
  }
  addTask(task) {
      this.isRunning = true
      this.total++;
      this.tasks.push(task)
      this.run()
  }
  pause() {
      this.isRunning = false
  }
  resume() {
      this.isRunning = false
      this.run()
  }
}

window.executor = new PromiseExecutor({
  retryDelay: 2000,
  timeout: 1000,
  current: 2,
  onProgress: (completed, total) => {
    console.log(`已完成${completed}/${total}`);
  },
  onSuccess: (success, total) => {
    console . log ( `已成功 ${success} / ${total} ` );
  },
  onError: (failed, total) => {
    console . warn ( `已失败 ${failed} / ${total} ` );
  },
});

const generateTask = (i) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const flag = Math.random() > 0.5;
      if (flag) {
        resolve(i + 1);
      } else {
        reject(i + 1);
      }
    }, 1000);
  });
};
const arr = new Array(10).fill(0).map((v, i) => () => generateTask(i));
arr.map((v) => {
  executor.addTask(v);
});

v2版本

1、支持重试次数,并且设定重试间隔

首先定义下重试的表现,本文的设计是,重试时,依然占用并发数,比如并发数为2,目前有一个任务正在重试,那么直到该任务完成之前,其他任务都依次执行,因为正在重试的任务并没有释放 current 变量,下文的资源均指的是 current 变量

2、支持超时控制

本文只实现对于首次请求的超时控制,对于重试时的超时则忽略,换句话说,首次请求时若超时,则视为超时,同时释放并发数,视为该任务已经处理完成

3、支持优先级队列

每次 addTask 时,对任务进行排序,目前采用直接调用 sort 的方式来简化处理,如果使用索引来做,需要考虑索引与当前执行位置的关系,有一定优化空间

class PromiseExecutor {
  constructor(config) {
    this.config = config;
    // 执行栈
    this.tasks = [];
    // 当前有几个任务在运行
    this.current = 0;
    // 完成了几个任务
    this.completed = 0;
    // 成功了几个任务
    this.success = 0;
    // 超时了几个任务
    this.timeout = 0
    // 失败了几个任务
    this.failed = 0;
    // 当前总共有多少任务
    this.total = 0;
    // 是否正在运行,用来实现暂停
    this.isRunning = false;
  }
  finallyCb() {
    this.current--;
    this.completed++;
    this.config.onProgress(this.completed, this.total);
    this.run();
  }
  async run() {
    // 如果被外界暂停了,return
    while (
      this.current < this.config.current &&
      this.tasks.length &&
      this.isRunning
    ) {
      const { task } = this.tasks.shift();
      if (task) {
        let hasTimeout = false;
        let timer = setTimeout(() => {
          this.timeout++
          this.config.onTimeout(this.timeout, this.total);
          hasTimeout = true;
          this.finallyCb();
          clearTimeout(timer)
        }, this.config.timeout);
        try {
          this.current++;
          await Promise.resolve(task());
          if (!hasTimeout) {
            timer && clearTimeout(timer);
            this.success++;
            this.config.onSuccess(this.success, this.total);
          }
        } catch (e) {
          if (!hasTimeout) {
            timer && clearTimeout(timer);
            console.log(e, "重试");
            await this.retry(task, this.config.maxRetry);
          }
        } finally {
          if (!hasTimeout) {
            timer && clearTimeout(timer);
            this.finallyCb();
          }
        }
      }
    }
  }
  async retry(task, retryCount) {
    let count = retryCount;
    this.isRunning = false;
    return new Promise((resolve) => {
      const handleRetry = async () => {
        if (!count) {
          this.failed++;
          this.config.onError(this.failed, this.total);
          this.isRunning = true;
          resolve();
          return;
        }
        try {
          const res = await Promise.resolve(task());
          this.isRunning = true;
          this.success++;
          this.config.onSuccess(this.success, this.total);
          console.log(res, "重试成功");
          resolve();
        } catch (e) {
          count--;
          setTimeout(() => {
            handleRetry();
          }, this.config.retryDelay);
        }
      };
      handleRetry();
    });
  }
  addTask({ task, priority = 0 }) {
    this.isRunning = true;
    this.total++;
    this.tasks.push({
      task,
      priority,
    });
    this.tasks.sort((a, b) => b.priority - a.priority);
    this.run();
  }
  pause() {
    this.isRunning = false;
  }
  resume() {
    this.isRunning = true;
    this.run();
  }
}

function generateDelay(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

window.executor = new PromiseExecutor({
  maxRetry: 1,
  retryDelay: 1000,
  timeout: 1000,
  current: 2,
  onTimeout: (timeout, total) => {
    console.log(`已超时${timeout}/${total}`);
  },
  onProgress: (completed, total) => {
    console.log(`已完成${completed}/${total}`);
  },
  onSuccess: (success, total) => {
    console.log(`已成功${success}/${total}`);
  },
  onError: (failed, total) => {
    console.warn(`已失败${failed}/${total}`);
  },
});
const generateTask = (i) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const flag = Math.random() > 0.5;
      if (flag) {
        console.log(i + 1, "success");
        resolve(i + 1);
      } else {
        console.warn(i + 1, "error");
        reject(i + 1);
      }
    }, generateDelay(500, 2000));
  });
};
const arr = new Array(10).fill(0).map((v, i) => () => generateTask(i));
arr.map((v) => {
  executor.addTask({
    task: v,
    priority: Math.random() > 0.5 ? 1 : 0,
  });
});

完整代码

class PromiseExecutor {
  constructor(config) {
    this.config = config;
    // 执行栈
    this.tasks = [];
    // 当前有几个任务在运行
    this.current = 0;
    // 完成了几个任务
    this.completed = 0;
    // 成功了几个任务
    this.success = 0;
    // 超时了几个任务
    this.timeout = 0
    // 失败了几个任务
    this.failed = 0;
    // 当前总共有多少任务
    this.total = 0;
    // 是否正在运行,用来实现暂停
    this.isRunning = false;
  }
  finallyCb() {
    this.current--;
    this.completed++;
    this.config.onProgress(this.completed, this.total);
    this.run();
  }
  async run() {
    // 如果被外界暂停了,return
    while (
      this.current < this.config.current &&
      this.tasks.length &&
      this.isRunning
    ) {
      const { task } = this.tasks.shift();
      if (task) {
        let hasTimeout = false;
        let timer = setTimeout(() => {
          this.timeout++
          this.config.onTimeout(this.timeout, this.total);
          hasTimeout = true;
          this.finallyCb();
          clearTimeout(timer)
        }, this.config.timeout);
        try {
          this.current++;
          await Promise.resolve(task());
          if (!hasTimeout) {
            timer && clearTimeout(timer);
            this.success++;
            this.config.onSuccess(this.success, this.total);
          }
        } catch (e) {
          if (!hasTimeout) {
            timer && clearTimeout(timer);
            console.log(e, "重试");
            await this.retry(task, this.config.maxRetry);
          }
        } finally {
          if (!hasTimeout) {
            timer && clearTimeout(timer);
            this.finallyCb();
          }
        }
      }
    }
  }
  async retry(task, retryCount) {
    let count = retryCount;
    this.isRunning = false;
    return new Promise((resolve) => {
      const handleRetry = async () => {
        if (!count) {
          this.failed++;
          this.config.onError(this.failed, this.total);
          this.isRunning = true;
          resolve();
          return;
        }
        try {
          const res = await Promise.resolve(task());
          this.isRunning = true;
          this.success++;
          this.config.onSuccess(this.success, this.total);
          console.log(res, "重试成功");
          resolve();
        } catch (e) {
          count--;
          setTimeout(() => {
            handleRetry();
          }, this.config.retryDelay);
        }
      };
      handleRetry();
    });
  }
  addTask({ task, priority = 0 }) {
    this.isRunning = true;
    this.total++;
    this.tasks.push({
      task,
      priority,
    });
    this.tasks.sort((a, b) => b.priority - a.priority);
    this.run();
  }
  pause() {
    this.isRunning = false;
  }
  resume() {
    this.isRunning = true;
    this.run();
  }
}

function generateDelay(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

window.executor = new PromiseExecutor({
  maxRetry: 1,
  retryDelay: 1000,
  timeout: 1000,
  current: 2,
  onTimeout: (timeout, total) => {
    console.log(`已超时${timeout}/${total}`);
  },
  onProgress: (completed, total) => {
    console.log(`已完成${completed}/${total}`);
  },
  onSuccess: (success, total) => {
    console.log(`已成功${success}/${total}`);
  },
  onError: (failed, total) => {
    console.warn(`已失败${failed}/${total}`);
  },
});
const generateTask = (i) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const flag = Math.random() > 0.5;
      if (flag) {
        console.log(i + 1, "success");
        resolve(i + 1);
      } else {
        console.warn(i + 1, "error");
        reject(i + 1);
      }
    }, generateDelay(500, 2000));
  });
};
const arr = new Array(10).fill(0).map((v, i) => () => generateTask(i));
arr.map((v) => {
  executor.addTask({
    task: v,
    priority: Math.random() > 0.5 ? 1 : 0,
  });
});