使用承诺时如何打破串行循环?

How to break out of a serial loop when using promises?

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

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

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() 数据库方法 returns 保存事件时解决的承诺。如果事件已经存在于数据库中,它将抛出异常,这会停止承诺链,因此文件的其余部分不会被解析。该异常被函数末尾的 catch() 处理程序捕获并忽略。我在 Node.js.

中使用 Bluebird 3.0 promise 库

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

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

解决方案?

建立在 , and taking into account 的基础上,也许我应该尝试他对我链接到的问题的非减少答案:),我想出了这个解决方案:

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() 中,该函数遍历数组中的每个项目。如果迭代器函数 returns 一个承诺,则在该承诺解决之前不会处理下一个项目。如果迭代器returnsfalse,则循环结束,Lo-dash style:

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 行文件是否会出现调用堆栈问题。

你所拥有的实际上根本没有跳出循环。

每次调用 Event.createAsync returns 都会立即成功,这意味着您总是减少 整个 数组。

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

这是此 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",因此不会浪费时间铺设轨道不需要。