【问题标题】:How to break out of a serial loop when using promises?使用 Promise 时如何跳出串行循环?
【发布时间】:2016-02-08 20:14:59
【问题描述】:

我有一个长文本文件,我逐行循环以提取一些事件数据并将其存储在数据库中。该文件会定期更新顶部的新数据。发生这种情况时,我会再次运行文件以提取新事件,但是当我遇到数据库中已经存在的事件时我想停止(文件总是按从最新到最旧的顺序排列)。

使用this answer中描述的reduce()方法对问题Correct way to write loops for promise,我想出了这个函数来解析文件:

function parse(
    file)
{
    var lines = file.split("\n"),
        latestDate;

    return lines.reduce(function(promise, line) {
        return promise.then(function() {
            if (/* line matches date pattern */) {
                latestDate = line;
            } else if (/* line matches event pattern */) {
                return Event.createAsync(line, latestDate);
            }

            return promise;
        });
    }, Promise.resolve())
        .catch({ errorName: "uniqueViolated" }, 
            function() { /* ignore only the createAsync error */ });
}

createAsync() 数据库方法返回一个在保存事件时解决的承诺。如果数据库中已经存在该事件,它将引发异常,这会停止承诺链,因此不会解析文件的其余部分。该异常被函数末尾的catch() 处理程序捕获并忽略。我在 Node.js 中使用 Bluebird 3.0 承诺库。

此函数会依次循环遍历每一行,并在遇到已保存的事件时正确停止。但我想知道这是否是在处理承诺时打破循环的最佳方式。在函数末尾吞下抛出的异常似乎有点笨拙。

欢迎提出任何改进循环处理的建议。

解决方案?

jib's answer 为基础,并考虑到Bergi's comment,也许我应该尝试他对我链接到的问题的非简化答案:),我想出了这个解决方案:

function parse(
    file)
{
    var lines = file.split("\n"),
        latestDate;

    return promiseEach(lines, function(line) {
        if (/* line matches date pattern */) {
            latestDate = line;
        } else if (/* line matches event pattern */) {
            return Event.createAsync(line, latestDate)
                .catch({ errorType: "uniqueViolated" }, function() { return false; });
        }
    });
}

循环递归被移到一个通用函数promiseEach() 中,该函数循环遍历数组中的每个项目。如果迭代器函数返回一个 Promise,则在该 Promise 解决之前不会处理下一项。如果迭代器返回false,则循环结束,Lo-dash 样式:

function promiseEach(
    list,
    iterator,
    index)
{
    index = index || 0;

    if (list && index < list.length) {
        return Promise.resolve(iterator(list[index])).then(function(result) {
            if (result !== false) {
                return promiseEach(list, iterator, ++index);
            }
        });
    } else {
        return Promise.resolve();
    }
}

我认为这可以满足我的要求,但我想知道如果我在 4000 行文件上运行它是否会出现调用堆栈问题。

【问题讨论】:

  • 我会按名称检查来自 createAsync 的特定错误,如果不同,则在 catch 中重新抛出错误。吞下所有错误是不好的。
  • 谢谢。我使用新的 Bluebird 3.0 对象谓词语法将.error() 更改为.catch(),因此它应该只会吞下createAsync() 引发的错误。我的问题仍然是依赖抛出的异常是否是一种合理的跳出循环的方法,或者是否有更规范的方法。
  • 抛出异常(或返回被拒绝的承诺)不仅是合理的,而且是唯一打破 .then 链(而不是构建它的循环)的方式。
  • 您可能只是尝试使用非reduce 方法来回答您链接的问题的其他答案:-)

标签: javascript node.js promise bluebird


【解决方案1】:

你所拥有的并没有真正打破循环。

Event.createAsync 的每次调用都会立即成功返回一个promise,这意味着您总是对整个 数组进行归约。

因此,此循环生成的承诺链的长度将始终是文件中的总行数,减去在您的特定逻辑中既不符合日期也不符合事件模式的行数。

这是该承诺链的异步执行,稍后会在由于数据库中已存在事件而引发错误时终止。

您的代码有效,但您说这是一个长文本文件,因此它可能效率低下,特别是如果提前中断是常态而不是例外(从您的描述中听起来)。

因此,我会考虑使用递归方法:

function parse(file) {
  var latestDate;

  function recurse(lines, i) {
    if (i >= lines.length) return Promise.resolve();

    var line = lines[i];
    if (/* line matches date pattern */) {
      latestDate = line;
    } else if (/* line matches event pattern */) {
      return Event.createAsync(line, latestDate).then(() => recurse(lines, i + 1));
    }
    return recurse(lines, i + 1);
  }

  return recurse(file.split("\n"), 0);
}

递归方法的好处是承诺链在Event.createAsync 解析时异步扩展,并且仅在需要时进行。您也可以只停止调用recurse 来停止,即不需要Event.createAsync 抛出异常来爆发。

一种可视化差异的方法可能是将其与为火车铺设轨道进行比较,其中轨道代表承诺链,而火车代表异步操作的执行承诺:

使用reduce,您总是在火车启动前先放下整条轨道,无论火车在异常停止之前最终沿着轨道走多远。每次铺设整条轨道的成本你都吃掉了(可能不多,但可以加起来)。

recurse 示例中,您正在移动的火车前及时铺设下一段轨道,例如Gromit in the finale of "The Wrong Trousers",因此无需浪费时间铺设不需要的轨道。

【讨论】:

  • 啊,你是对的。我自欺欺人地认为它没有通过在return promise.then(...) 块中记录该行来循环每一行。在引发第一个异常后,这些行停止记录。但是将日志调用放在该块之前确实表明reduce() 循环了整个文件,正如我应该预料的那样。
  • 实际上,在调用Event.createAsync(...).then(...) 之后,您需要一个.catch({ errorType: "uniqueViolated" }, function() {}) 处理程序来捕获当事件已经在数据库中时引发的异常。否则,promise 链将中断,但我们希望将非唯一性错误视为正常并继续该链。
  • 当然,但无论如何它不会再做任何行了,对吧?所以无论你在这里还是更远的地方都可以找到它。
  • 对,但是parse() 函数应该是自包含的,所以我可以做parse(file).then(...) 而不必担心捕获它之外的唯一性异常。
猜你喜欢
  • 1970-01-01
  • 2012-01-23
  • 1970-01-01
  • 2015-05-27
  • 1970-01-01
  • 2011-12-22
  • 2020-08-08
  • 2015-08-16
  • 1970-01-01
相关资源
最近更新 更多