问题 Javascript:如何使用promises迭代数组?


现场演示

鉴于以下功能:

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;
}

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


6148
2017-08-12 11:58


起源

好吧,因为你只对第一个好的数字感兴趣,你的方法似乎很好,否则一个实现使用 q.all 本来会更合适。然而, shift 是昂贵的,所以我会考虑不改变数组,只是增加一个索引。函数名称中没有任何内容暗示这一点 numbers 将被改变,所以如果你坚持使用该解决方案,至少要复制它 numbers.slice()。如果你有很多数字(足以填充调用堆栈),那么使用堆栈进行迭代。 - plalx
在检查下一个号码之前检查每个号码是否很重要,或者是否允许一次检查所有号码以找到第一个尽可能快的号码? - Willem D'Haeseleer
如果你提前禁止迭代数组,你所拥有的是接近最佳解决方案,如果你可以提前迭代数组(但不检查它),这可以简化为for循环。 - Benjamin Gruenbaum
@Misha Moroshko可以用良好的功能解决他收到的号码吗? - Narek Mamikonyan


答案:


我想知道是否有更好的方法?

是。避免 延迟反模式

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 实际上可行和可并行化的程度,这可能是“更好”。但是,它可能会做很多不必要的工作;您可能希望使用支持取消的promise库。

使用Bluebird库的一个例子,它有一个 any 辅助功能 致力于这项任务:

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

9
2017-08-12 12:16



不切片阵列会很好 n 时间,也可以使用 .catch 代替 .then(null,  因为它是Angular。 - Benjamin Gruenbaum
非递归解决方案每次都会迭代整个数组。 - Benjamin Gruenbaum
@BenjaminGruenbaum:补充道。切片是delibaretely,我来自功能背景,讨厌修改我的输入:-) - Bergi
@Bergi感谢您的回答。只有一件事:它应该是递归版本 return findGoodNumber(numbers); 代替 findGoodNumber(numbers);。 - Misha Moroshko


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

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而获得的最佳效果。


2
2017-08-12 12:25



你应该避免第一个 isGood(arr[i++]).catch( 并使用命名的IIFE,否则它将不适用于空数组。 - Bergi
@Bergi是的,我把它固定在一个空的承诺开始,无论如何可能更好:) - Benjamin Gruenbaum


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

演示

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;
  }
});

1
2017-08-12 13:41



使用的目的是什么 map 这里?如果你不需要结果数组( undefineds?),你应该使用 .forEach 或一个简单的循环。 - Bergi
你说得对,让我更新 - Narek Mamikonyan


承诺从未打算用作布尔,但这实际上是什么 isGood() 是在做。在这里,我们不仅仅意味着使用布尔值来解析/拒绝承诺。我们的意思是 承诺的状态 传达其状态:

  • 待定==尚未知
  • 已解决== true
  • 拒绝== false

有些人可能认为这是承诺滥用,但试图以这种方式利用承诺是很有趣的。

可以说,有关承诺的主要问题是布尔值:

  • 'true'的promise表示将采用成功路径,'false'的promise表示将采用失败路径
  • 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();
}

我们需要一种“非”方法 - 交换“解决”和“拒绝”的方法。 jQuery承诺没有原生逆变器,所以这里有一个功能来完成这项工作。

/* 
 * 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应该非常简单。

说明

else 的条款 findGoodNumber() 可能不太明显。它的核心是 numbers.reduce(...),建立一个 .then() 链 - 有效地进行同步扫描 numbers 阵列。这是一种熟悉的异步模式。

在没有两个反转的情况下,阵列将被扫描直到第一个 坏号码 找到并导致拒绝将采用失败路径(跳过扫描的其余部分并继续执行失败处理程序)。

但是,我们希望找到第一个 好的号码 采取“失败”的道路 - 因此需要:

  • 内部反转:将报告的“true”转换为“false” - 强制跳过其余的扫描
  • 外部反转:恢复原始的布尔意义 - “true”最终为“true”,“false”最终为“false”。

你可能需要搞砸了 演示 更好地了解正在发生的事情。

结论

是的,没有递归就可以解决问题。

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

替代解决方案

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

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());
    }
}

这是jQuery相当于Bergi的解决方案。

DEMO


0
2017-08-12 22:29



你为什么用 not(AND[promises.map(not)])?你只是可以做到 OR[promises] 通过使用 then链的错误处理程序。 - Bergi
@Bergi,是的,就像常规的布尔逻辑一样。皮猫的方法不止一种。我的想象力使我朝着一个方向发展,你的想象力引领着我。 - Roamer-1888