要点:
- 店家可以设置任意张数的N人桌,根据自家容客量来设置,并且初始化时会给每张桌子一个编号。
初始化桌子的数目以及能使用的人数:
2人桌:3张, 4人桌:9张, 6人桌:6张, 8人桌:4张
const deskSettings = [
{ peopleNum: 2, maxExecutingNum: 3 },
{ peopleNum: 4, maxExecutingNum: 9 },
{ peopleNum: 6, maxExecutingNum: 6 },
{ peopleNum: 8, maxExecutingNum: 4 },
];
const scheduler = new Scheduler(deskSettings);
Scheduler内部初始化,根据N人桌来构造相应的任务管理器,因此会构造4个管理器(2人,4人,6人,8人),这4个管理器会放在taskMapper映射表中。当需要取用时,会从映射表中拿出来
//peopleNum表示几人桌,maxTaskNum表示这家店有多少个这样的桌子
const letterStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
class Scheduler {
constructor(TaskSettings) {
if ((TaskSettings||[]).length > 0) {
this.taskMapper = {};
for (const item of TaskSettings) {
let { peopleNum, maxExecutingNum } = item;
if (peopleNum <= 0) {
continue;
}
let taskObj = {
tasks: [],
executingTasks: [],
maxTaskNum: maxExecutingNum,
idList: []
};
let letter = letterStr.substr(peopleNum-1,1);
for (let i = 1; i <= maxExecutingNum; i++) {
let idStr = letter + (i.toString().length<2 ? '0'+i : i);
taskObj.idList.push(idStr);
}
this.taskMapper['peopleNum_'+peopleNum] = taskObj;
}
} else {
this.tasks = []; //待执行的任务列表
this.executingTasks = []; //正在执行的任务列表
this.maxTaskNum = maxTaskNum; //最大执行任务数
}
}
}
构造出来的taskMapper结构如下:tasks,待执行的任务列表;executingTasks,正在执行的任务列表,maxTaskNum,最大执行任务数,
idList,所有桌子的编号
{
peopleNum_2: {
tasks: [],
executingTasks: [],
maxTaskNum: 3,
idList: [ 'B01', 'B02', 'B03' ]
},
peopleNum_4: {
tasks: [],
executingTasks: [],
maxTaskNum: 9,
idList: ['D01', 'D02', 'D03', 'D04', 'D05', 'D06', 'D07', 'D08', 'D09' ]
},
peopleNum_6: {
tasks: [],
executingTasks: [],
maxTaskNum: 6,
idList: [ 'F01', 'F02', 'F03', 'F04', 'F05', 'F06' ]
},
peopleNum_8: {
tasks: [],
executingTasks: [],
maxTaskNum: 4,
idList: [ 'H01', 'H02', 'H03', 'H04' ]
}
}
- 由于每桌人的用餐时长不一样,因此为了模拟真实情况,会随机设定顾客的用餐时长:
//顾客随机用餐的时长:秒
const dinnerTimeArr = [10,20,30,40];
//从数组中随机取出元素,N不能大于数组的长度
function getRandomArrayElements(arr, N) {
let indexArray = [];
arr = arr || [];
N = N || 0;
if (arr.length === 0) {
return [];
}
let max = arr.length - 1;
let min = 0;
for (let i = 0; i < N; i++) {
let randomIndex = parseInt(Math.random() * (max - min + 1)) + min; //取随机数组索引值
while (indexArray.includes(randomIndex)) {
randomIndex = parseInt(Math.random() * (max - min + 1)) + min;
}
indexArray.push(randomIndex);
}
let randomArray = indexArray.map(index => arr[index]);
return randomArray;
}
以及用eatDinnerPromise来表示顾客的用餐行为:
function eatDinnerPromise(peopleNum, deskNum) {
return new Promise((resolve, reject) => {
let dinnerTime = (getRandomArrayElements(dinnerTimeArr,1))[0];
console.log(`我们是${peopleNum}人桌,桌子编号【${deskNum}】,【${moment().format('HH:mm:ss')}】开始用餐,将会进行${dinnerTime}秒。`);
let ts = dinnerTime*1000;
setTimeout(()=> {
console.log(`我们用餐结束【${moment().format('HH:mm:ss')}】,桌子编号【${deskNum}】,用时:${dinnerTime}秒`);
resolve();
},ts);
});
}
- 店家的桌子数据初始化完毕,顾客用餐行为也准备完毕,接下来就需要把这些代码串联起来。首先来了一桌顾客,先把他们加入系统,Scheduler中应有一个add方法,
add(promiseMaker, peopleNum) {
let task = this.generateTask(promiseMaker, peopleNum);
let taskObj = this.taskMapper['peopleNum_'+peopleNum];
if (taskObj.executingTasks.length < taskObj.maxTaskNum) {
this.run(task, peopleNum);
} else {
taskObj.tasks.push(task);
}
return task.id;
}
而由于外部加进来的promise不一定符合我们的数据结构需求,就需要用内部方法generateTask来转换一下,
generateTask(promiseMaker, peopleNum){
let taskObj = this.taskMapper['peopleNum_'+peopleNum];
//从生成的数据中随机拿一个id编号出来
let taskId = this.getTaskId(taskObj.idList);
return {
id: taskId,
peopleNum: peopleNum,
promise: promiseMaker
};
}
当需要从N人桌的编号池中拿一个编号出来时,会需要用到getTaskId方法,为了模拟真实情况,从编号池中拿取是采用的随机拿取,并且会暂时的从编号池移除此编号,当用餐结束再把编号归还。
getTaskId(idList){
let taskId = (getRandomArrayElements(idList, 1))[0];
idList.splice(idList.findIndex(id => id === taskId), 1);
return taskId;
}
以上讲的只是如何把外面promise封装成内部需要的任务结构,接下来就是运行任务,当正在执行的任务列表有空余时,会直接把任务加入此列表运行,使用的是run方法
run(task, peopleNum) {
let taskObj = this.taskMapper['peopleNum_'+peopleNum];
let arrayLength = taskObj.executingTasks.push(task);
let index = arrayLength - 1;
task.promise(peopleNum, task.id).then(() => {
taskObj.executingTasks.splice(index, 1);
//归还id编号
taskObj.idList.push(task.id);
if (taskObj.tasks.length > 0) {
let newTask = taskObj.tasks.shift();
newTask.id = this.getTaskId(taskObj.idList);
this.run(newTask, newTask.peopleNum);
}
})
}
这个任务结束之后,会做3件事:
(1)从正在执行的任务列表executingTasks中把自己移除
(2)归还任务(桌子)编号
(3)若待执行任务列表tasks中有任务,会从头部取出一个新任务,并从之前移除任务的编号池里拿出一个编号给新任务运行
- 为了了解排在自己前面还有多少桌,会用show来展示排队信息:
//获取现在排在前面的桌子数目
show(peopleNum,deskId){
let taskObj = this.taskMapper['people_Num'+peopleNum];
let index = taskObj.tasks.findIndex(item => item.id === deskId);
let executingTaskNum = taskObj.executingTasks.length;
console.log(`您的${peopleNum}人桌编号为${deskId},有${executingTaskNum}在吃`);
console.log(`排在您前面的还有${index}桌`);
return {
preTaskNum: index,
executingTaskNum: executingTaskNum
};
}
以上就把Scheduler构造完毕了,接着用一些例子来测试下成果:
scheduler.add(eatDinnerPromise,2);
scheduler.add(eatDinnerPromise,2);
scheduler.add(eatDinnerPromise,2);
scheduler.add(eatDinnerPromise,2);
scheduler.add(eatDinnerPromise,2);
打印出来的结果:
我们是2人桌,桌子编号【B02】,【23:27:39】开始用餐,将会进行20秒。
我们是2人桌,桌子编号【B03】,【23:27:39】开始用餐,将会进行30秒。
我们是2人桌,桌子编号【B01】,【23:27:39】开始用餐,将会进行20秒。
我们用餐结束【23:27:59】,桌子编号【B02】,用时:20秒
我们是2人桌,桌子编号【B02】,【23:27:59】开始用餐,将会进行40秒。
我们用餐结束【23:27:59】,桌子编号【B01】,用时:20秒
我们是2人桌,桌子编号【B01】,【23:27:59】开始用餐,将会进行20秒。
我们用餐结束【23:28:09】,桌子编号【B03】,用时:30秒
我们用餐结束【23:28:19】,桌子编号【B01】,用时:20秒
完整的代码链接:点餐排队系统Demo
结语:
根据【带并发限制的异步调度器】可以做出一个能用于实际场景的Demo例子,这就是举一反三的实际运用。
当然Demo肯定不能用于实际的,只能是演示可行性,若真正需要投入实际场景使用,还需要更多的扩展,如:操作界面可视化,配置数据持久化,手动选择用餐桌子,打印排队信息,统计日均用餐数等。
如果练手这个项目,我会采用Egg+React来实践,到时候做出来会分享在github上,敬请期待。