如何在使用promises时打破串行循环?

时间:2015-11-07 21:15:41

标签: javascript node.js promise bluebird

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

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

此功能会串行循环遍历每一行,并在遇到已保存的事件时正确停止。但是我想知道这是否是在处理承诺时突破循环的最佳方法。在函数结束时吞下抛出的异常似乎有些麻烦。

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

解?

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

1 个答案:

答案 0 :(得分:4)

你所拥有的东西实际上并没有完全脱离循环。

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

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

这个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解析时,只有在需要时才会异步扩展promise链。你也可以停止调用recurse来停止,即Event.createAsync不需要抛出异常来突破。

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

使用reduce时,您始终会在列车开始前首先放下整个轨道,无论列车在异常停止之前最终沿着轨道走多远。你每次都要花掉铺设整条赛道的费用(可能不多,但可以加起来)。

recurse示例中,您正在移动的火车前准时铺设下一条轨道,如Gromit in the finale of "The Wrong Trousers",所以没有时间浪费铺设不会有的轨道需要。