【问题标题】:JavaScript : Calling Recursive Functions With PromisesJavaScript:使用 Promise 调用递归函数
【发布时间】:2016-01-10 22:14:15
【问题描述】:

语言:JavaScript


递归 - 不是我最喜欢的主题。

Promises - 它们可能会让人感到困惑。

递归 + 承诺 - 我需要在有软垫的房间里编程。

我制作了这个我称之为 The RecursiveFunHouse 的小 JS Fiddle 拼图,作为通过将问题简化为愚蠢的事情来保持理智的喜剧方式。希望你们能从我的痛苦中笑出来:)

问题:

每个递归调用都依赖于前一个调用的结果,但为了获得结果,我必须运行一个异步任务并将该结果用于其他子任务。

“Recursive Fun House”帮助我将问题归结为这个问题 - 由于子任务仍在执行,原始递归循环以未定义的值继续。

The Fun House - 循环收集 (-99) 和 99 之间的随机数。一旦最后一个数字与新数字之间的最后一个差为正数,乐趣就结束了

打印“Made it here 1...Made it here 6”应该表明子任务得到了正确处理,并且我们有下一个循环的值。

当前它打印 1,2,3,6,4,5 :(

recursiveFunHouse.js

var recursiveFunHouse = function(num){
    console.log("Made it here 1");
    var newNum = performSideTasks();

    console.log("Made it here 6");
    console.log("newNum");
    console.log(newNum);
    if(newNum-num >0 ){
            recursiveFunHouse(newNum);
    }
    else{
        console.log("The FunHouse Generated These Numbers :")
      for(var i = 0; i <numList.length; i++){
         console.log(numList[i]);
      }
    }

};

var performSideTasks = function(){
    console.log("Made it here 2");
    someAsyncTask().then(function(num){
            anotherTask(num);
      console.log("made it here 5");
      return num;
        });


}

var someAsyncTask = function(){
  return new Promise (function(resolve, reject) {
    console.log("made it here 3");
    var randNum = Math.floor(Math.random()*99) + 1; 
    randNum *= Math.floor(Math.random()*2) == 1 ? 1 : -1;   

    setTimeout(function() { 
      numList.push(randNum)
      resolve(randNum)
    }, 100);
  });
}




var anotherTask = function(num){
  console.log("made it here 4");
    console.log(num);

};

var numList= [];

recursiveFunHouse(20);

注意 - 原谅我可悲的退货声明return num; 它只是显示了我希望我能告诉我的计算机的内容。

关于递归和承诺的问题:

1) 首先我是否应该担心进入​​下一个递归循环,而承诺尚未解决?

2) 如果不是,那么保持此函数分解并强制每个递归循环等待最后一次调用的解决的干净方法是什么?


递归和承诺有时看起来像巫师级的 95 级魔法......这就是我要说的。问题完成。

【问题讨论】:

  • performSideTasks 应该返回 someAsynTask Promise 本身,以便主函数可以获取 .then() 内部的 num 值

标签: javascript recursion


【解决方案1】:

在经典同步递归中,递归状态存储在堆栈帧中,从一个公共执行堆栈推入和弹出。在异步递归中,递归状态可以存储在从普通 Promise 链的头部推送和弹出的 Promise 对象中。例如:

function asyncThing( asyncParam) { // example operation
  const promiseDelay = (data,msec) => new Promise(res => setTimeout(res,msec,data));
  return promiseDelay( asyncParam, 1000); //resolve with argument in 1 second.
}

function recFun( num) { // example "recursive" asynchronous function

    // do whatever synchronous stuff that recFun does when called
    //      ...
    // and decide what to do with async result: recurse or finish?

    function decide( asyncResult) {
        // process asyncResult here as needed:
        console.log("asyncResult: " + asyncResult); 
        if( asyncResult == 0)
            console.log("ignition");

        // check if further recursion is needed:
        if( asyncResult < 0)
            return "lift off"; // no, all done, return a non-promise result
        return recFun( num-1); // yes, call recFun again which returns a promise
    }

    // Return a promise resolved by doing something async and deciding what to do.
    // to be clear the returned promise is the one returned from the .then call

    return asyncThing(num).then(decide);
}

// call the recursive function
recFun( 5)
.then( function(result) {console.log("done, result = " + result); })
.catch( function(err) {console.log("oops:" + err);});

运行代码看看效果。

本示例所依赖的核心原则(魔法):

  1. then 注册监听器函数返回一个未决的承诺。如果一个监听器被调用并从执行中返回,监听器返回值将解析待处理的承诺。如果侦听器抛出错误而不是返回,则挂起的 Promise 会被抛出的值拒绝。
  2. 不能通过承诺来履行承诺。如果一个监听器返回一个 Promise,它会被插入到 Promise 链剩余部分的头部,在从监听器注册返回的 Promise 之前。然后,来自监听器注册的承诺(之前是剩余承诺链的头部)与插入的承诺同步,最终确定时采用其状态和值。
  3. 当定义链的代码执行时,所有 Promise 对象和 Promise 链的链接都是同步创建的。然后定义代码运行完成(意味着它返回到事件循环而不会被异步回调中断,因为 JavaScript 是单线程的)。

如果以及当执行侦听器函数时(因为承诺被履行或拒绝),它们会在导致侦听器被执行的代码自身运行完成之后,在事件循环中自己调用中异步执行。

这意味着在注册 Promise 侦听器(通过在 Promise 上调用 then)时生成的 所有 日志条目出现在执行时注册的侦听器函数生成的 任何 日志条目之前异步稍后。承诺不涉及时间旅行。

这会让你不再头疼吗?也许不是,但至少是真的。

`

【讨论】:

  • 全面、启发性的解释和例子!
  • 很好的解释。我将在 JavaScript 中添加类似的内容,只需将 'thread' 替换为 'turn of the event loop'
  • @DeanRadcliffe 好点。这是一个 JavaScipt 答案,但我使用“线程”来表示从事件循环中调用是不正确的。已更新答案以消除混淆的根源。感谢您的评论。
  • @traktor53 谢谢,你的回答对我帮助很大。
  • 在客户端合并服务器端分页的完美代码,其中递归数取决于页数(下一个/下一个/下一个)。谢谢!
【解决方案2】:

如评论中所述,您必须稍微修改 performSideTasks 以使其返回 Promise:

var performSideTasks = function () 
{
  console.log( "Made it here 2" )
  return someAsyncTask().then( function ( num )
  {
    anotherTask(num);
    console.log("made it here 5")
    return num
  } )
} 

那么就可以在main函数的then()方法中使用异步结果了。

var recursiveFunHouse = function ( num )
{
  console.log( "Made it here 1" )
  performSideTasks().then( function ( newNum )
  {
    console.log( "Made it here 6" )
    console.log( "newNum" )
    console.log( newNum )
    if ( newNum-num > 0 )
    {
      recursiveFunHouse( newNum )
    }
    else
    {
      console.log( "The FunHouse Generated These Numbers :" )
      for( var i = 0 ; i <numList.length ; i++ )
      {
        console.log( numList[i] )
      }
    }
  } )
}

【讨论】:

  • @Supersharp:我注意到您使用的indent style 与我见过的大多数代码不同。这不一定是坏事,但请注意您选择的样式 - 由于 Javascript 中的automatic semicolon insertion,它可能会导致您意想不到的错误。
猜你喜欢
  • 1970-01-01
  • 2016-05-26
  • 1970-01-01
  • 1970-01-01
  • 2020-02-10
  • 1970-01-01
  • 2017-03-25
  • 1970-01-01
相关资源
最近更新 更多