forEach 循环中的 promise.all —— 一切立即触发

Posted

技术标签:

【中文标题】forEach 循环中的 promise.all —— 一切立即触发【英文标题】:promise.all inside a forEach loop — everything firing at once 【发布时间】:2016-09-02 12:55:34 【问题描述】:

在 Node 应用程序中,我需要以同步方式遍历某些项目,但循环内的某些操作是异步的。我的代码现在看起来像这样:

someAPIpromise().then((items) => 
   items.forEach((item) => 
      Promise.all[myPromiseA(item), myPromiseB(item)]).then(() => 
         doSomethingSynchronouslyThatTakesAWhile();
      );
    

items 是一个 1 的数组时,这会产生奇迹。但是,一旦有多个项目,promise.all() 将立即为数组中的每个项目触发,而无需等待循环中的操作结束。

说了这么多...如何确保数组中每个项目的整个操作同步运行(即使某些操作是异步的并返回一个承诺)?

非常感谢!

没有

【问题讨论】:

您是否尝试过将items 传递给Promise.all(),使用.reduce() 进行迭代;并删除.forEach()? 不确定我是否遵循...items 中的每个item 应该是它自己的同步操作。您是否建议一次解决所有项目的承诺? items 中的元素是函数吗? 将值推送到数组;如果items数组中的元素是函数,则调用函数,如果元素是Promise,则返回Promise,如果元素既不是函数也不是Promise,则返回值包裹在Promise.resolve() 【参考方案1】:

您正在构建多个 Promise,但它们都是异步的。你构建了 Promise1、Promise2、Promise3,......但是一旦它们在野外,它们就会同时开火。如果您想要同步行为,您必须将它们链接在一起,以便 Promise1 的 .then() 执行 Promise2 等等。过去我为此使用过 Array.reduce。

someAPIpromise().then((items) => 
    items.reduce((accumulator, current) =>
        accumulator.then(() =>
             Promise.all[myPromiseA(item), myPromiseB(item)]).then(() => 
                 doSomethingSynchronouslyThatTakesAWhile();
             )
        )
    , Promise.resolve());

如果你愿意,你可以把它写成一个辅助函数,这可能会让事情更清楚。

function execSequentially (arr, func) 
    return arr.reduce(
        (accumulator, current) => accumulator.then(() => func(current)), 
        Promise.resolve());

那个函数被执行为

execSequentially(items, item => console.log(item));

当然用你想做的替换console.log。

辅助函数方法对更改的侵入性也较小。应用于您的原始代码的助手:

someAPIpromise().then((items) => 
   execSequentially(items, (item) =>
      Promise.all[myPromiseA(item), myPromiseB(item)]).then(() => 
         doSomethingSynchronouslyThatTakesAWhile();
      );
   );
);

【讨论】:

很好的解释,@Paarth!使用reduce 非常有意义......唉,仍然没有运气,数组中的每个项目都在同时触发。事实上,我测试它的方式是这样的:首先,在 Promise.all 之前放一个 console.log,然后在 thenable 之后放一个 console.log,我看到“嘿,我开始这里”都在触发同时关闭,所有异步调用(例如,数组有两个项目 ---> 总共四个异步调用)同时关闭。 @napo,你确定要return来自 Promise.all 的内在承诺吗?请注意,如果您有需要显式返回的块,那么像我在示例中得到的表达式体 lambdas 会隐式执行它。抱歉回复晚了 没有问题!请参阅下面的答案以了解最终如何解决。实际上,与您所拥有的非常接近。谢谢! @napo,您是否在我的答案中使用了带有表达式主体的 lambda 的代码?我正在查看您标记为答案的内容,它看起来几乎完全相同。唯一的区别是您正在构建另一个承诺并使用它的解析功能,这应该等同于我正在做的只是返回 Promise.all 承诺。 在某种程度上,是的......但我必须返回一个新的承诺,这是您的 sn-p 中缺少的内容,因此导致一切异步触发。我给了它+1,因为它仍然是最终结果的良好基础。非常感谢!【参考方案2】:

你应该可以删除.forEach();使用Array.prototype.reduce()Promise 值的数组返回给Promise.all()。如果带有items 的元素是一个函数,则调用函数,否则包裹在Promise.resolve() 中,它应该以与items 数组中相同的顺序返回结果

Promise.all()

Promise.all 传递来自所有承诺的值数组 它被传递的可迭代对象。值数组保持 原始可迭代对象的顺序,而不是承诺的顺序 被解决了。如果在可迭代数组中传递的东西不是 承诺,它被Promise.resolve转换为1。

var arr = [1, // not asynchronous
  function j() 
    return new Promise(function(resolve) 
      setTimeout(function() 
        resolve(2)
      , Math.floor(Math.random() * 10000))
    )
  , // asynchronous
  3, // not asynchronous
  function j() 
    return new Promise(function(resolve) 
      setTimeout(function() 
        resolve(4)
      , Math.floor(Math.random() * 3500))
    )
  , // asynchronous
  5, // not asynchronous
  Promise.resolve(6), // asynchronous
  7
];

Promise.all(arr.reduce(function(p, next) 
    var curr = Promise.resolve(typeof next === "function" ? next() : next);
    return p.concat.apply(p, [curr.then(function(data) 
      console.log(data);
      return data
    )]);
  , []))
  .then(function(data) 
    console.log("complete", data)
  )

另一种方法是使用Array.prototype.shift()Promise.resolve().then()、递归

function re(items, res) 
  if (items.length) 
    var curr = items.shift();
    return Promise.resolve(
      typeof curr === "function" 
      ? curr() 
      : curr
    ).then(function(data) 
      // values from `arr` elements should be logged in sequential order
      console.log(data);
      res.push(data)
    ).then(re.bind(null, items, res))
   else 
    return ["complete", res]
  


var _items = arr.slice(0);

re(_items, [])
.then(function(complete) 
  console.log(complete)
)

var arr = [1, // not asynchronous
  function j() 
    return new Promise(function(resolve) 
      setTimeout(function() 
        resolve(2)
      , Math.floor(Math.random() * 10000))
    )
  , // asynchronous
  3, // not asynchronous
  function j() 
    return new Promise(function(resolve) 
      setTimeout(function() 
        resolve(4)
      , Math.floor(Math.random() * 3500))
    )
  , // asynchronous
  5, // not asynchronous
  Promise.resolve(6), // asynchronous
  7
];

function re(items, res) 
  if (items.length) 
    var curr = items.shift();
    return Promise.resolve(
      typeof curr === "function" 
      ? curr() 
      : curr
    ).then(function(data) 
      // values from `arr` elements should be logged in sequential order
      console.log(data);
      res.push(data)
    ).then(re.bind(null, items, res))
   else 
    return ["complete", res]
  

var _items = arr.slice(0);
re(_items, [])
  .then(function(complete) 
    console.log(complete)
  )

【讨论】:

我有点明白你在做什么,谢谢你的解释!也就是说......你是说我应该承诺数组中的每个项目,然后在整个数组上调用promise.all @napo 在数组上调用Promise.all(),无论值是Promise 还是Promise,使用.reduce() 来调用函数或返回值。如果原始值不是Promise,则不需要对值使用Promise.resolve()new Promise() 构造函数。结果仍将按照.then()Promise.all() 之后的原始数组中的项目顺序返回。例如,如果您将第一个setTimeout 持续时间调整为10000,则2 仍应按顺序返回到.then() 之后的.all() @napo items 中的承诺或值是否在发布时使用js 按顺序解决? 感谢您的跟进。是的,项目按顺序调用。也就是说,我可以看到每个Promise.all() 的相同项目同时执行。恰当的例子:其中一种承诺方法从 HTTP 资源下载少量资产。如果我查看网络流量,我会看到与数组中的两个项目有关的资产正在下载,这是不应该发生的——我应该能够在 promise.all() 中为项目 1 下载资产,然后执行我的同步操作,然后继续第 2 项。有意义吗? “我应该能够在 promise.all() 中为第 1 项下载资产,然后执行我的同步操作,然后转到第 2 项。有意义吗?”不要相信仅使用Promise.all() 就可以保证返回值的顺序是按顺序排列的。尽管在解决前一项之前,第二种方法不应继续处理数组中的下一项。【参考方案3】:

好吧...我们能够让它工作的方式:array.reduce() 在 Promises 的帮助下。最终结果:

myAsyncAPIcall.then(items => 
    items.reduce((current, nextItem) => 
        return current.then(() => 
          return new Promise(res => 
             Promise.all([myPromiseA(nextItem), myPromiseB(nextItem]).then(() => 
               someSynchronousCallThatTakesAWhile(nextItem);
               res();
             ).catch(err => 
                   console.log(err);
             );
          );
        );
    , Promise.resolve())
)

它的工作方式是,通过将数组的每一项包装在自己的Promise(resolve, reject) 中,我们可以确保每次迭代同步运行,因为完成一次迭代将触发需要解析下一个 Promise,并且等等等等。在每个 Promise 解析中,调用可以尽可能多地异步启动,并且知道在它完成之前它们只会作用于父 Promise。

希望对大家有所帮助!

【讨论】:

如果您删除它而不是返回 new Promise(res => ...) 而只是 return Promise.all(...) 这对您有用吗?【参考方案4】:

保留forEach怎么样...

var stopAllProcessingOnServerLowValue= false;

function someAPIpromise()
    var arr = [
        id:123, urlVal:null,
        id:456, urlVal:null,
        id:789, urlVal:null,
        id:101112, urlVal:null
    ];

    return new Promise(function(resolve)
        setTimeout(function()
            resolve(arr)
        , 3000);
    )


function extractSomeValueRemotely(url)
    return new Promise(function(resolve, reject)
        console.log("simulate an async connection @ %s to request a value", url);
        setTimeout(function()
            var someRandom = Math.round(Math.random()*7) + 1;
            console.log("%s responded with %s", url, someRandom);
            if(someRandom > 4)
                resolve(someRandom);
            
            else
                var issue = "Urls result is too low ("+someRandom+" <= 4).";
                console.warn(issue+".It will be set to -1");
                if(stopAllProcessingOnServerLowValue)
                    reject(issue+".Operation rejected because one or mole server results are too low ["+someRandom+"].");
                
                else
                    resolve(-1);
                
            
        , 1500*Math.round(Math.random()*7) + 1);
    );


function addAnotherExtraParamToItem(_item)
    return new Promise(function(resolve, reject)
        setTimeout(function()
            console.log("setting extra2 on %s", _item.id);
            _item['extra'] = "additional_processing_"+_item.id;
            resolve(_item);
        , 1500*Math.round(Math.random()*5) + 1);
    );


function addOrderIndexToItem(_item, _order)
    return new Promise(function(resolve, reject)
        setTimeout(function()
            console.log(">> setting order %s on %s",_order,  _item.id);
            _item['order'] = _order;
            resolve(_item);
        , 1500*Math.round(Math.random()*3) + 1);
    );


someAPIpromise().then(function(items)

    var perItemPromises = [];
    items.forEach(function(item, idx)

        perItemPromises.push(

            new Promise(function(pulseItemResolve, pulseItemReject)
                var itemStepsPromises =  [];
                itemStepsPromises.push(addAnotherExtraParamToItem(item));

                itemStepsPromises.push(extractSomeValueRemotely("http://someservice:777/serve-me")
                    .catch(
                        function(reason)
                            //the entire item will be rejected id
                            pulseItemReject(reason);
                        )
                );

                itemStepsPromises.push(addOrderIndexToItem(item, idx));

                //promise that ensure order of execution on all previous async methods
                Promise.all(itemStepsPromises).then(function(values)
                    //0 - first is result from addAnotherExtraParamToItem
                    var theItem = values[0]; //it returns the item itself
                    //urlVal has not been set yet

                    // 1 - second promise return the url result
                    var serverResult = values[1];

                    //2 - third promise add the order index but we do not care to inspect it because theItem reference in value[0] has been already updated.
                    // console.info(values[2]);

                    //sets the url result in the item
                    theItem.urlVal = serverResult;
                    console.log("urlVal set to:", theItem.urlVal);

                    //resolve the prepared item
                    pulseItemResolve(theItem);

                );
            )
                .catch(function(reason)
                    //escalate error
                    throw new Error(reason);
                )
        )

    );

    Promise.all(perItemPromises).then(function(resultsInAllItems)
        console.info("Final results:");
        console.info(resultsInAllItems);
    ).catch(function(finalReject)
        console.error("Critical error:",finalReject);
    )


);

【讨论】:

【参考方案5】:

经过大量研究,对我来说确定的答案就在这里......

我已经阅读了一堆有用的纯 javascript(无插件)-Promise Iterator- 可以在我的项目中轻松使用(一行)的解决方案,最后我由 Salketer 找到 this solution

function one_by_one(objects_array, iterator, callback) 
    var start_promise = objects_array.reduce(function (prom, object) 
        return prom.then(function () 
            return iterator(object);
        );
    , Promise.resolve()); // initial
    if(callback)
        start_promise.then(callback);
    else
        return start_promise;
    

有关详细信息和使用示例,请访问the link。

它还允许直接处理回调。

这是我在与 Promise 迭代和测试来自许多问题、博客和官方网站的多个解决方案进行多日斗争后发现的最合乎逻辑和可重复使用的方法。

如果您还在为一个明确的答案而苦苦挣扎,请尝试一下。

【讨论】:

以上是关于forEach 循环中的 promise.all —— 一切立即触发的主要内容,如果未能解决你的问题,请参考以下文章

与 Promise.all() 中的解析相比,为啥在 while 循环中单独解析 Promise 数组时解析更慢? [复制]

在 forEach 之后使用 Promise.all() 渲染 NodeJS 页面之前等待 Firebase 数据加载

使用 Promise.all 避免等待每个单独的循环迭代

循环 await Promise.all 内存泄漏

如何在 useEffect 挂钩中使用 Promise.all 获取所有数据?

Promise.all() 与等待