【问题标题】:async await inside setTimeout inside forEachasync await 在 setTimeout 里面 forEach
【发布时间】:2020-07-18 00:07:13
【问题描述】:

我需要在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。

【问题讨论】:

  • 在您添加“一种可能的解决方案”之前,我写了一个显示该选项的答案。这是你最好的选择。
  • 我知道,我和你在同一时间写它:)。我知道它是最好的解决方案,我只是想知道是否可以使用 setTimeout 和 forEach 来实现。顺便说一句,感谢您的回答和您的时间。
  • 没有。 .forEach() 不关心它的回调返回什么,所以它只是盲目地运行迭代的所有循环,而无需等待任何异步操作完成。因此,没有办法让它做你想做的事情。此外,带有awaitfor 循环看起来也更简单,您可以知道什么时候完成。这只是一个更好的选择。
  • 但我正在寻找更好的方法 以何种方式更好?这正是您想要的,并且兼具可读性和高效性。除非您希望两个循环同时运行?
  • 你应该解释为什么你想要forEach而不是for of,否则你应该使用for of

标签: javascript node.js async-await


【解决方案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() 并不关心你传递给它的回调返回什么,所以它完全不知道承诺,因此在使用承诺时真的没有任何位置。常规的for 循环非常有用,因为它会在使用await 时暂停循环。


有一些方法可以通过.forEach() 使用在循环外部声明的更高范围的 Promise 来破解东西,但是这些选项根本不干净,所以我不会费心在这里展示它们。


在我们使用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() 脚手架都妨碍了查看控制流。

【讨论】:

    【解决方案2】:

    如果你真的想要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");
    
    
    })()

    【讨论】:

      【解决方案3】:

      我不确定您为什么要明确使用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 进行更干净的for循环。


      你可以通过定义一个像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");
      

      【讨论】:

      • 当然,您可以使用循环外部的变量对.forEach() 进行破解。这不是重点。此外,这不会在一个问题的完成和下一个问题的开始之间插入延迟。
      • @jfriend00 这取决于你所说的黑客。 forEach 仅用于改变状态,没有其他用途,所以我不会称其为 hack。
      • @JohnathanBarclay 问题中的问题是下一个工作在前一个工作完成之前开始。这个答案确实解决了问题。
      • @3limin4t0r 是的,我明白你现在在做什么。
      猜你喜欢
      • 1970-01-01
      • 2016-01-22
      • 1970-01-01
      • 2018-07-09
      • 1970-01-01
      • 2021-06-20
      • 2021-08-25
      相关资源
      最近更新 更多