【问题标题】:Javascript: How to iterate on array using promises?Javascript:如何使用承诺迭代数组?
【发布时间】:2014-10-05 11:43:15
【问题描述】:

LIVE DEMO

给定以下函数:

function isGood(number) {
  var defer = $q.defer();

  $timeout(function() {
    if (<some condition on number>) {
      defer.resolve();
    } else {
      defer.reject();
    }
  }, 100);

  return defer.promise;
}

和一个数字数组(例如[3, 9, 17, 26, 89]),我想找到第一个“好”的数字。我希望能够做到这一点:

var arr = [3, 9, 17, 26, 89];

findGoodNumber(arr).then(function(goodNumber) {
  console.log('Good number found: ' + goodNumber);
}, function() {
  console.log('No good numbers found');
});

这是实现此功能的一种可能的递归版本:DEMO

function findGoodNumber(numbers) {
  var defer = $q.defer();

  if (numbers.length === 0) {
    defer.reject();
  } else {
    var num = numbers.shift();

    isGood(num).then(function() {
      defer.resolve(num);
    }, function() {
      findGoodNumber(numbers).then(defer.resolve, defer.reject)
    });
  }

  return defer.promise;
}

我想知道是否有更好的(可能是非递归的)方式?

【问题讨论】:

  • 好吧,既然你只对第一个好的数字感兴趣,你的方法似乎很好,否则使用q.all 的实现会更合适。但是,shift 很昂贵,所以我会考虑不改变数组并简单地增加索引。函数名称中没有任何内容暗示numbers 会发生变异,因此如果您坚持使用该解决方案,至少使用numbers.slice() 复制它。如果您有很多数字(足以填满调用堆栈),请使用堆栈的迭代方法。
  • 在检查下一个数字之前检查每个数字是否重要,还是允许一次检查所有数字以尽快找到第一个好的?
  • 如果你禁止提前迭代数组,你所拥有的接近最佳解决方案,如果你可以提前迭代数组(但不检查它),这可以简化为 for 循环。
  • @Misha Moroshko 可以用他收到的号码解析 isGood 函数吗?

标签: javascript angularjs promise q angular-promise


【解决方案1】:

不知道有没有更好的办法?

是的。避免deferred antipattern

function isGood(number) {
  return $timeout(function() {
    if (<some condition on number>) {
      return number; // Resolve with the number, simplifies code below
    } else {
      throw new Error("…");
    }
  }, 100);
}
function findGoodNumber(numbers) {
  if (numbers.length === 0) {
    return $q.reject();
  } else {
    return isGood(numbers.shift()).catch(function() {
      return findGoodNumber(numbers);
    });
  }
}

可能是非递归的?

您可以制定一个链接大量then 调用的循环,但是递归在这里绝对没问题。如果你真的想要这个循环,它可能看起来像这样:

function findGoodNumber(numbers) {
  return numbers.reduce(function(previousFinds, num) {
    return previousFinds.catch(function() {
      return isGood(num);
    });
  }, $q.reject());
}

然而这效率较低,因为它总是查看所有numbers。 “递归”版本会懒惰地评估它,并且只有在当前数字不好时才会进行另一次迭代。

也许更快?

您可以并行触发所有isGood 检查,并等待第一个完成的返回。根据isGood 的实际作用以及可并行化的程度,这可能会“更好”。但是,它可能会做很多不必要的工作;您可能想要使用支持取消的承诺库。

使用 Bluebird 库的示例,其中有一个专用于此任务的 any helper function

function findGoodNumber(numbers) {
  return Bluebird.any(numbers.map(isGood))
}

【讨论】:

  • 最好不要对数组进行切片n 次,也可以使用.catch 而不是.then(null, ,因为它是Angular。
  • 非递归解决方案每次都会迭代整个数组。
  • @BenjaminGruenbaum:添加。切片是故意完成的,我来自功能背景,讨厌修改我的输入:-)
  • @Bergi 感谢您的回答。只有一件事:在递归版本中,它应该是 return findGoodNumber(numbers); 而不是 findGoodNumber(numbers);
【解决方案2】:

这里是一种具有不同递归形式的替代解决方案:

function firstGood(arr){
    var i = 0;
    return $q.when().then(function consume(){
        if(i >= arr.length) return $q.reject(Error("No Number Found"));
        return isGood(arr[i++]).catch(consume);
    });
}

它与 Bergi 所拥有的非常相似,并且它是在不像某些库(Bluebird 和最近的 When)那样实现 Promise.reduce 的情况下获得的最好的。

【讨论】:

  • 你应该避免使用第一个 isGood(arr[i++]).catch( 并使用命名的 IIFE,否则它将不适用于空数组。
  • @Bergi 是的,我把它改成从一个空洞的承诺开始,无论如何这可能更好:)
【解决方案3】:

这是我的版本,只需使用 array.map 函数

Demo

angular.module('MyApp', []).run(function($q, $timeout) {
  var arr = [3, 9, 17, 26, 89];

  findGoodNumber(arr).then(function(goodNumber) {
    console.log('Good number found: ' + goodNumber);
  }, function() {
    console.log('No good numbers found');
  });

  function findGoodNumber(numbers) {
    var defer = $q.defer();

    numbers.forEach(function(num){      
      isGood(num).then(function(){
        defer.resolve(num);
      });

    });

    return defer.promise;
  }

  function isGood(number) {
    var defer = $q.defer();

    $timeout(function() {
      if (number % 2 === 0) {
        defer.resolve();
      } else {
        defer.reject();
      }
    }, 1000);

    return defer.promise;
  }
});

【讨论】:

  • 这里使用map的目的是什么?如果你不需要结果数组(undefineds?),你应该使用.forEach 或一个简单的循环。
【解决方案4】:

Promise 从来不打算用作布尔值,但这实际上是 isGood() 正在做的事情。在这里,我们不仅仅是指用布尔值解决/拒绝一个承诺。我们的意思是,promise 的状态 传达了它的状态:

  • 待定 == 未知
  • 已解决 == 正确
  • 拒绝 == 错误

有些人可能认为这是滥用 Promise,但尝试以这种方式利用 Promise 很有趣。

可以说,关于 Promise 作为布尔值的主要问题是:

  • “true”的承诺表示将采用成功路径,“false”的承诺表示将采用失败路径
  • Promise 库自然不会允许所有必要的布尔代数 - 例如。非、与、或、异或

在更好地探索和记录该主题之前,需要想象力来克服/利用这些功能

让我们尝试解决这个问题(使用 jQuery - 我更了解它)。

首先让我们写一个更明确的isGood()

/*
 * A function that determines whether a number is an integer or not
 * and returns a resolved/rejected promise accordingly.
 * In both cases, the promise is resolved/rejected with the original number.
 */ 
function isGood(number) {
    return $.Deferred(function(dfrd) {
        if(parseInt(number, 10) == number) {
            setTimeout(function() { dfrd.resolve(number); }, 100);//"true"
        } else {
            setTimeout(function() { dfrd.reject(number); }, 100);//"false"
        }
    }).promise();
}

我们将需要一个“NOT”方法——交换“已解决”和“已拒绝”的方法。 jQuery Promise 没有本地逆变器,所以这里有一个函数来完成这项工作。

/* 
 * A function that creates and returns a new promise 
 * whose resolved/rejected state is the inverse of the original promise,
 * and which conveys the original promise's value.
 */ 
function invertPromise(p) {
    return $.Deferred(function(dfrd) {
        p.then(dfrd.reject, dfrd.resolve);
    });
}

现在是问题findGoodNumber() 的一个版本,但这里利用了重写的isGood()invertPromise() 实用程序。

/*
 * A function that accepts an array of numbers, scans them,
 * and returns a resolved promise for the first "good" number,
 * or a rejected promise if no "good" numbers are present.
 */ 
function findGoodNumber(numbers) {
    if(numbers.length === 0) {
        return $.Deferred.reject().promise();
    } else {
        return invertPromise(numbers.reduce(function(p, num) {
            return p.then(function() {
                return invertPromise(isGood(num));
            });
        }, $.when()));
    }
}

最后,相同的调用例程(数据略有不同):

var arr = [3.1, 9.6, 17.0, 26.9, 89];
findGoodNumber(arr).then(function(goodNumber) {
    console.log('Good number found: ' + goodNumber);
}, function() {
    console.log('No good numbers found');
});

DEMO

将代码转换回 Angular/$q 应该很简单。

说明

findGoodNumber()else 子句可能不太明显。它的核心是numbers.reduce(...),它构建了一个.then() 链——实际上是对numbers 数组的异步扫描。这是一种熟悉的异步模式。

在没有两次反转的情况下,将扫描数组,直到找到第一个 错误数字,并且结果拒绝将采用失败路径(跳过扫描的其余部分并继续失败处理程序)。

然而,我们想找到第一个好的数字来走“失败”的道路——因此需要:

  • 内部反转:将报告的“真”转换为“假” - 强制跳过扫描的其余部分
  • 外部反转:恢复原来的布尔意义——“真”以“真”结尾,“假”以“假”结尾。

您可能需要弄乱demo 以更好地了解正在发生的事情。

结论

是的,不用递归就可以解决问题。

这个解决方案既不是最简单的也不是最有效的,但它希望展示了 Promise 状态表示布尔值和实现异步布尔代数的潜力。

替代解决方案

findGoodNumber() 可以通过执行“或扫描”来编写,而无需反转,如下所示:

function findGoodNumber(numbers) {
    if(numbers.length === 0) {
        return $.Deferred.reject().promise();
    } else {
        return numbers.reduce(function(p, num) {
            return p.then(null, function() {
                return isGood(num);
            });
        }, $.Deferred().reject());
    }
}

这是 Bergi 解决方案的 jQuery 等价物。

DEMO

【讨论】:

  • 你为什么使用not(AND[promises.map(not)])?您只需使用then 的错误处理程序链接即可完成OR[promises]
  • @Bergi,是的,它就像常规的布尔逻辑一样。给猫剥皮的方法不止一种。我的想象力将我引向了一个方向,而你的想象力将你引向了另一个方向。
猜你喜欢
  • 1970-01-01
  • 2016-12-01
  • 1970-01-01
  • 1970-01-01
  • 2017-04-08
  • 2021-03-07
  • 2018-05-27
  • 2018-10-19
  • 2016-11-13
相关资源
最近更新 更多