Javascript:如何使用承诺迭代数组?

时间:2021-05-18 01:23:16

LIVE DEMO

现场演示

Given the following function:

鉴于以下函数:

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

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

  return defer.promise;
}

and an array of numbers (e.g. [3, 9, 17, 26, 89]), I would like to find the first "good" number. I would like to be able to do this:

以及一系列的数字(例如[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');
});

Here is one possible recursive version to implement this: DEMO

这里有一个实现这个的可能的递归版本: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;
}

I wonder if there is a better (maybe non-recursive) way?

我想知道是否有更好的(也许是非递归的)方法?

4 个解决方案

#1


9  

I wonder if there is a better way?

我想知道有没有更好的办法?

Yes. Avoid the 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);
    });
  }
}

maybe non-recursive?

也许非递归?

You can formulate a loop that chains lots of then calls, however recursion is absolutely fine here. If you really wanted the loop, it might look like this:

您可以形成一个循环,将许多调用连接在一起,但是递归在这里绝对没问题。如果你真的想要循环,它可能是这样的:

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

This is however less efficient, as it always looks at all numbers. The "recursive" version will evaluate it lazily, and only do another iteration if the current number was not good.

然而,这并不那么有效,因为它总是关注所有的数字。“递归”版本将缓慢地对其进行评估,并且只有在当前数字不太好时才进行另一次迭代。

maybe faster?

也许更快?

You can fire all isGood checks in parallel, and wait for the first fulfilled to return. Depending on what isGood actually does and how well that is parallelizable, this might be "better". It potentially does a lot of unnecessary work, though; you may want to use a promise library that supports cancellation.

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

An example using the Bluebird library, which has a any helper function dedicated to this task:

使用“蓝鸟图书馆”(Bluebird library)的一个例子,该图书馆有专门用于这项任务的任何辅助功能:

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

#2


2  

Here is an alternative solution with a different form of recursion:

下面是另一种递归形式:

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

It's pretty similar to what Bergi has and it's about the best you can get without implementing a Promise.reduce like some libraries (Bluebird and more recently When) have.

这和Bergi所拥有的非常相似,它是你在不履行承诺的情况下所能得到的最好的东西。减少像一些图书馆(蓝鸟和最近的时候)有。

#3


1  

this is my version by simply using array.map function

这是我使用数组的版本。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;
  }
});

#4


0  

Promises were never meant to be used as booleans but that's effectively what isGood() is doing. And here, we don't just mean resolving/rejecting a promise with a boolean value. We mean that the state of a promise conveys its state :

承诺从来就不应该被用作布尔人,但isGood()实际上就是这么做的。在这里,我们不只是要用布尔值解析/拒绝一个承诺。我们的意思是,承诺的状态传达了它的状态:

  • pending == as yet unknown
  • 等待==还不知道。
  • resolved == true
  • 解决了= = true
  • rejected == false
  • 拒绝了= =假

Some might regard this as promise abuse, but it's good fun trying to exploit promises in this way.

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

Arguably the main issues concerning promises as booleans are :

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

  • The promise representation of 'true' will take the success path and the promise representation of 'false' will take the fail path
  • “真”的承诺表示将走向成功,“假”的承诺表示将走向失败
  • Promise libraries don't naturally allow for all the necessary boolean algebra - eg. NOT, AND, OR, XOR
  • 承诺库不会自然地允许所有必要的布尔代数。不,或者,XOR

Until this topic is better explored and documented, it will take imagination to overcome/exploit these features.

在更好地探索和记录这个主题之前,需要想象力来克服/利用这些特性。

Let's try and solve the problem (with jQuery - I know it much better).

让我们尝试解决这个问题(使用jQuery——我知道它更好)。

First let's write a more definite version of isGood() :

首先让我们写一个更明确的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();
}

We are going to need a "NOT" method - something that swaps 'resolved' and 'rejected'. jQuery promises don't have a native inverter, so here's a function to do the job.

我们需要一种“不”的方法——一种交换“解决”和“拒绝”的方法。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);
    });
}

Now, a version of the question's findGoodNumber(), but here exploiting the rewritten isGood() and the invertPromise() utility.

现在,一个版本的问题的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()));
    }
}

And finally, the same calling routine (with slightly different data) :

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

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

演示

It should be quite simple to convert the code back to Angular/$q.

将代码转换回角度/$q应该非常简单。

Explanation

解释

The else clause of findGoodNumber() is maybe less than obvious. The core of it is numbers.reduce(...), which builds a .then() chain - effectively an asychronous scan of the numbers array. This is a familiar async pattern.

findGoodNumber()的else子句可能不太明显。它的核心是数字。reduce(…),它构建一个then()链-有效的数字阵列的异步扫描。这是一种常见的异步模式。

In the absence of the two inversions, the array would be scanned until the first bad number was found and the resulting rejection would take the failure path (skipping the remainder of the scan and proceeding to the fail handler).

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

However, we want to find the first good number to take the "failure" path- hence the need for :

然而,我们希望找到第一个选择“失败”路径的好数字——因此需要:

  • the inner inversion: to convert reported "true" to "false" - forcing the rest of the scan to be skipped
  • 内部反转:将报告的“真”转换为“假”——强制跳过其余的扫描
  • the outer inversion: to restore the original bloolean sense - "true" ends up as "true" and "false" ends up as "false".
  • 外部反演:为了恢复原本血腥的感觉——“真”以“真”结束,“假”以“假”结束。

You may need to mess around with the demo to better appreciate what's going on.

为了更好地理解正在发生的事情,你可能需要把演示弄得一团糟。

Conclusion

结论

Yes, it's possible to solve the problem without recursion.

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

This solution is neither the simplest nor the most efficient, however it hopefully demonstrates the potential of promises' state to represent booleans and to implement asynchronous boolean algebra.

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

Alternative solution

findGoodNumber() can be written without needing to invert by performing an "OR-scan", as follows :

可以编写findGoodNumber(),而不需要执行“OR-scan”,如下所示:

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

This is the jQuery equivalent of Bergi's solution.

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

DEMO

演示

#1


9  

I wonder if there is a better way?

我想知道有没有更好的办法?

Yes. Avoid the 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);
    });
  }
}

maybe non-recursive?

也许非递归?

You can formulate a loop that chains lots of then calls, however recursion is absolutely fine here. If you really wanted the loop, it might look like this:

您可以形成一个循环,将许多调用连接在一起,但是递归在这里绝对没问题。如果你真的想要循环,它可能是这样的:

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

This is however less efficient, as it always looks at all numbers. The "recursive" version will evaluate it lazily, and only do another iteration if the current number was not good.

然而,这并不那么有效,因为它总是关注所有的数字。“递归”版本将缓慢地对其进行评估,并且只有在当前数字不太好时才进行另一次迭代。

maybe faster?

也许更快?

You can fire all isGood checks in parallel, and wait for the first fulfilled to return. Depending on what isGood actually does and how well that is parallelizable, this might be "better". It potentially does a lot of unnecessary work, though; you may want to use a promise library that supports cancellation.

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

An example using the Bluebird library, which has a any helper function dedicated to this task:

使用“蓝鸟图书馆”(Bluebird library)的一个例子,该图书馆有专门用于这项任务的任何辅助功能:

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

#2


2  

Here is an alternative solution with a different form of recursion:

下面是另一种递归形式:

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

It's pretty similar to what Bergi has and it's about the best you can get without implementing a Promise.reduce like some libraries (Bluebird and more recently When) have.

这和Bergi所拥有的非常相似,它是你在不履行承诺的情况下所能得到的最好的东西。减少像一些图书馆(蓝鸟和最近的时候)有。

#3


1  

this is my version by simply using array.map function

这是我使用数组的版本。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;
  }
});

#4


0  

Promises were never meant to be used as booleans but that's effectively what isGood() is doing. And here, we don't just mean resolving/rejecting a promise with a boolean value. We mean that the state of a promise conveys its state :

承诺从来就不应该被用作布尔人,但isGood()实际上就是这么做的。在这里,我们不只是要用布尔值解析/拒绝一个承诺。我们的意思是,承诺的状态传达了它的状态:

  • pending == as yet unknown
  • 等待==还不知道。
  • resolved == true
  • 解决了= = true
  • rejected == false
  • 拒绝了= =假

Some might regard this as promise abuse, but it's good fun trying to exploit promises in this way.

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

Arguably the main issues concerning promises as booleans are :

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

  • The promise representation of 'true' will take the success path and the promise representation of 'false' will take the fail path
  • “真”的承诺表示将走向成功,“假”的承诺表示将走向失败
  • Promise libraries don't naturally allow for all the necessary boolean algebra - eg. NOT, AND, OR, XOR
  • 承诺库不会自然地允许所有必要的布尔代数。不,或者,XOR

Until this topic is better explored and documented, it will take imagination to overcome/exploit these features.

在更好地探索和记录这个主题之前,需要想象力来克服/利用这些特性。

Let's try and solve the problem (with jQuery - I know it much better).

让我们尝试解决这个问题(使用jQuery——我知道它更好)。

First let's write a more definite version of isGood() :

首先让我们写一个更明确的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();
}

We are going to need a "NOT" method - something that swaps 'resolved' and 'rejected'. jQuery promises don't have a native inverter, so here's a function to do the job.

我们需要一种“不”的方法——一种交换“解决”和“拒绝”的方法。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);
    });
}

Now, a version of the question's findGoodNumber(), but here exploiting the rewritten isGood() and the invertPromise() utility.

现在,一个版本的问题的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()));
    }
}

And finally, the same calling routine (with slightly different data) :

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

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

演示

It should be quite simple to convert the code back to Angular/$q.

将代码转换回角度/$q应该非常简单。

Explanation

解释

The else clause of findGoodNumber() is maybe less than obvious. The core of it is numbers.reduce(...), which builds a .then() chain - effectively an asychronous scan of the numbers array. This is a familiar async pattern.

findGoodNumber()的else子句可能不太明显。它的核心是数字。reduce(…),它构建一个then()链-有效的数字阵列的异步扫描。这是一种常见的异步模式。

In the absence of the two inversions, the array would be scanned until the first bad number was found and the resulting rejection would take the failure path (skipping the remainder of the scan and proceeding to the fail handler).

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

However, we want to find the first good number to take the "failure" path- hence the need for :

然而,我们希望找到第一个选择“失败”路径的好数字——因此需要:

  • the inner inversion: to convert reported "true" to "false" - forcing the rest of the scan to be skipped
  • 内部反转:将报告的“真”转换为“假”——强制跳过其余的扫描
  • the outer inversion: to restore the original bloolean sense - "true" ends up as "true" and "false" ends up as "false".
  • 外部反演:为了恢复原本血腥的感觉——“真”以“真”结束,“假”以“假”结束。

You may need to mess around with the demo to better appreciate what's going on.

为了更好地理解正在发生的事情,你可能需要把演示弄得一团糟。

Conclusion

结论

Yes, it's possible to solve the problem without recursion.

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

This solution is neither the simplest nor the most efficient, however it hopefully demonstrates the potential of promises' state to represent booleans and to implement asynchronous boolean algebra.

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

Alternative solution

findGoodNumber() can be written without needing to invert by performing an "OR-scan", as follows :

可以编写findGoodNumber(),而不需要执行“OR-scan”,如下所示:

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

This is the jQuery equivalent of Bergi's solution.

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

DEMO

演示