在forEach内部的setTimeout内部异步等待

时间:2020-07-17 14:33:17

标签: javascript node.js async-await

我需要在nodejs中安排一些后台作业,因此我需要在上一个作业完成后才能运行。

在函数中,我有下面的代码。

jobArray.forEach((item, i) => {
  setTimeout(async () => {
    await collectChannelsData(item)
  }, i * 10000);
});
console.log('go to 2nd job')

jobArray.forEach((item, i) => {
  setTimeout(async () => {
    await collectVideosData(item)
  }, i * 10000);
});
console.log('finish')

问题是,因为setTimeout是一个异步函数,forEach不会等待它完成,而是继续执行第二项工作。有什么可能的解决方案吗?

提前谢谢

编辑:一种可能的解决方案是将我的代码转换为:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

for (const job of jobArray) {
  await collectChannelsData(job) 
  await wait(10000);
}
for (const job of jobArray) {
  await collectVideosData(job) 
  await wait(10000);
}

但是我正在寻找一种包括setTimeout和forEach的方法。

3 个答案:

答案 0 :(得分:1)

注意:这个问题最初是问 “我正在寻找更好的方法” ,它最初不包含使用await wait(10000)的示例。我写了这个原始问题的答案(在编辑之前)。此后,该问题已被编辑为其他问题。

但是我正在寻找一种包括setTimeout和forEach的方法。

没有.forEach()来做到这一点的干净方法,而不会涉及循环外的变量和可能的链接承诺。 .forEach()不会以任何方式感知诺言,因此,当您要对循环的异步迭代进行排序时,通常最好放弃使用它。

针对for使用await循环是解决此类问题的较好选择之一。它满足了对异步操作进行排序的主要目的,即在完成一个异步操作与开始下一个异步操作之间有特定的延迟。它简单,易读,高效,易于维护,为调用者提供了一种方法,使他们知道何时完成所有操作,并将异步错误返回给调用者。您的.forEach()循环不符合您的目标或做许多其他事情。

您可以将计时器设置为可返回承诺的函数,可以将await与以下命令配合使用:

function delay(t) {
    return new Promise(resolve => setTimeout(resolve, t));
}

async function run() {

    for (let item of jobArray) {
        await collectChannelsData(item)
        await delay(10000);
    }
    // go to second job now that first job is finished
    for (let item of jobArray) {
        await collectVideosData(item);
        await delay(10000);
    }
}

run().then(() => {
   console.log("all done");
}).catch(err => {
   console.log(err);
});

您可能已经知道,.forEach()并没有关注传递给它的回调返回的内容,因此它是完全不承诺的,因此在使用promises时确实没有任何位置更多。常规的for循环是如此有用,因为使用await时它将暂停循环。


有一些方法可以使用在循环外部声明的范围更大的承诺来使用.forEach()入侵事物,但是这些选项根本不是干净的,因此我不会在这里显示它们。


在拥有await之前,我们曾经使用.reduce()对诺言进行排序。这会创建一个promise链,这些promise会依次调用它们的功能:

function delay(t) {
    return new Promise(resolve => setTimeout(resolve, t));
}

// use .reduce() to chain promises so async operations are sequenced
jobArray.reduce(p, item => {
    return p.then(() => {
         return collectChannelsData(item).then(() => delay(10000));
    });
}, Promise.resolve()).then(() => {
    jobArray.reduce(p, item => {
        return p.then(() => {
            return collectVideosData(item).then(() => delay(10000));
        });
    }, Promise.resolve());
}).then(() => {
   console.log("all done");
}).catch(err => {
   console.log(err);
});

但是希望您可以看到await实现会更干净,因为所有.reduce()支架都妨碍了控制流的实现。

答案 1 :(得分:0)

如果您真的想使用forEach方法,则必须编写自己的forEach异步版本

(async function() {

  const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

  async function forEach(arr, cb) {
    for (const item of arr) {
      await cb(item);
    }
  }

  await forEach([1, 2, 3], async function(item) {
    await wait(500);
    console.log("Step" + item);
  });

  console.log("Done");


})()

答案 2 :(得分:-1)

我不确定您为什么显式地使用forEach,因为它不是很漂亮。但是绝对可以使用forEach

let queue = Promise.resolve();

jobArray.forEach(job => {
  queue = queue.then(() => collectChannelsData(job));
});

jobArray.forEach(job => {
  queue = queue.then(() => collectVideosData(job));
});

queue.then(() => console.log("finish"));
// or if you are inside an async function
await queue;
console.log("finish");

即使由于承诺彼此等待,还是出于某种原因仍要在其中添加延迟(setTimeout)。您可以通过将循环更改为以下方式来实现:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

jobArray.forEach((job, i) => {
  queue = queue.then(() => collectChannelsData(job))
               .then(() => wait(10000 * i));
});

我个人将await设为answered by jfriend00,进入清洁循环。


您可以通过定义诸如Queue之类的帮助程序类来使上述内容更简洁,该类将执行很多重复的代码queue = queue.then(...)

class Queue {
  constructor(initial) {
    this.promise = Promise.resolve(initial);
  }

  then(onFulfilled, onRejected) {
    return this.promise.then(onFulfilled, onRejected);
  }

  add(task) {
    this.promise = this.then(task);
  }
}

const queue = new Queue();
jobArray.forEach(job => queue.add(() => collectChannelsData(job)));
jobArray.forEach(job => queue.add(() => collectVideosData(job)));

await queue; // <- assuming you are in async function context
console.log("finish");