如何利用async.eachSeries实现setTimeout异步函数串行执行?
你遇到的核心问题是队列里存储的是已经启动的定时器ID,而不是待执行的任务逻辑,所以eachSeries遍历的时候,两个定时器直接同时启动了,自然达不到串行效果。我们需要调整任务存储和eachSeries的使用方式,让它能真正控制每个任务的执行时机。
问题拆解
你的getNextDialogTimer方法在被调用时就立刻启动了定时器,并返回定时器ID,这意味着当你把它push到队列里的时候,定时器已经在运行了,eachSeries根本没机会去控制顺序。
修正方案
我们需要把**任务配置(而不是已启动的定时器)**存入队列,然后在eachSeries里逐个执行任务,确保前一个任务的所有异步操作(定时器+next()的Promise)完成后,再启动下一个任务。
步骤1:调整队列存储内容
不要直接调用getNextDialogTimer,而是把需要的参数(dialog和ms)作为对象存入队列:
// 入队代码:存任务配置,而不是已启动的定时器 this.dialogTimerQueue.push({ dialog: data, ms: 1000 }); this.dialogTimerQueue.push({ dialog: data2, ms: 1000 });
步骤2:正确使用async.eachSeries
eachSeries的第二个任务函数需要接收两个参数:当前任务的配置,以及一个回调函数cb。你需要在定时器触发后,等待next()的Promise完成,再调用cb(),这样eachSeries才会执行下一个任务。
修正后的遍历代码:
async.eachSeries(this.dialogTimerQueue, (task, cb) => { // 启动当前任务的定时器 setTimeout(() => { console.log(`开始处理对话框: ${task.dialog}`); // 等待next()的Promise resolve后,通知eachSeries执行下一个任务 this.next(task.dialog) .then(() => { console.log(`对话框处理完成: ${task.dialog}`); cb(); // 调用cb,告诉eachSeries可以进行下一个任务了 }) .catch((err) => { // 别忘了处理错误,避免eachSeries卡住 console.error('对话框处理出错:', err); cb(err); }); }, this.npcDialogDelay * task.ms); }, (err) => { // 所有任务完成后的回调 if (err) { console.error('串行任务执行失败:', err); } else { console.log('所有对话框任务已串行执行完成'); } });
步骤3:(可选)简化getNextDialogTimer
如果不需要复用定时器逻辑,其实可以把getNextDialogTimer的逻辑直接整合到eachSeries的任务函数里,这样更清晰。如果需要复用,可以把它改成返回一个接收回调的函数:
getNextDialogTimer: function(dialog, ms, cb) { setTimeout(() => { this.next(dialog) .then(() => cb()) .catch((err) => cb(err)); }, this.npcDialogDelay * ms); }
然后在eachSeries里调用:
async.eachSeries(this.dialogTimerQueue, (task, cb) => { this.getNextDialogTimer(task.dialog, task.ms, cb); }, (err) => { // 任务完成回调 });
为什么这样能串行?
async.eachSeries的工作机制是:
- 执行第一个任务,直到任务调用
cb() - 只有当
cb()被调用后,才会开始执行第二个任务 - 以此类推,直到所有任务完成
在我们的修正代码里,cb()是在setTimeout触发,并且this.next()的Promise完全resolve之后才调用的,这就保证了:
- 第一个定时器等待时间到了,才会触发
next() next()的异步操作完成后,才会启动第二个定时器- 完全符合你需要的串行执行逻辑
内容的提问来源于stack exchange,提问作者user3871

