实现一个 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,
});
});