JavaScript: 使用 Promises 调用递归函数

18

语言:JavaScript


递归 - 不是我的最爱话题。

Promise - 它们可以变得混乱。

递归 + Promise - 我需要在一个带垫子的房间里编程。

我做了这个小的JS Fiddle谜题,我称之为 递归乐园,这是一种把问题简化成有趣的事情来保持理智的喜剧方式。希望你们能从我的痛苦中笑出来 :)

问题:

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

"递归乐园"帮助我将问题归纳为 - 原始递归循环在继续使用未定义值作为子任务仍在执行。

乐园 - 循环收集(-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; 表达了我希望能告诉计算机的内容。

有关递归和 Promise 的问题:

1)我是否应该担心在承诺未解决的情况下进入下一个递归循环?

2)如果不用担心,有没有一种干净的方法来保持这个函数的分解,并强制每个递归循环等待上一次调用的解决?


递归和 Promise 有时似乎是巫师级别95的魔法…这就是我想说的。问题解决了。


performSideTasks应该返回someAsynTask Promise本身,以便主函数可以在.then()中获取num值。 - Supersharp
2个回答

28

在经典的同步递归中,递归状态存储在推入和弹出共同执行栈的堆栈帧中。在异步递归中,递归状态可以存储在 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。如果调用了一个监听器并从执行中返回,那么监听器的返回值就会解决这个待定的 promise。如果监听器抛出错误而不是返回值,则将使用抛出的值拒绝待定的 promise。
  2. Promise 无法通过Promise实现。如果监听器返回一个promise,则该Promise插入剩余的Promise链的头部,在来自监听器注册的Promise之前。然后,来自监听器注册的Promise(先前是剩余Promise链的头)与插入的Promise同步,当最终解决时采用其状态和值。
  3. 一个Promise链的所有Promise对象和链接都是在定义该链的代码同步创建的。然后定义代码运行到完成(表示它在没有被 JavaScript 单线程异步回调中断的情况下返回事件循环)。

如果listener函数被异步执行(因为Promise变为resolved或rejected),它们会以异步方式在独立于调用listener的代码之后的自己的调用中执行。

这意味着所有的注册Promise监听器时进行的日志记录都在稍后异步执行的已注册监听器函数进行任何日志记录之前。Promises不涉及时间旅行。

这会让您的头痛停止吗?也许不,但至少是真实的。


全面、启发性的解释和示例! - Velojet
很好的解释。我会在JavaScript中加入类似的内容,只需将“线程”替换为“事件循环的轮次”。 - Dean Radcliffe
@DeanRadcliffe 很好的观点。这是一个JavaScript答案,但我使用“线程”来表示从事件循环中调用是不正确的。已更新答案以消除混淆的来源。感谢您的评论。 - traktor
@traktor53 谢谢,你的回答对我帮助很大。 - flyingace
1
完美的代码,可将服务器端分页合并到客户端,其中递归次数取决于页面数量(下一页/下一页/下一页)。谢谢! - gabn88

11

正如评论中所述,您需要稍微修改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
  } )
} 

然后,您可以在主函数的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] )
      }
    }
  } )
}

4
@Supersharp: 我注意到你使用了一种不同于我见过的大多数代码的 缩进风格。这并不一定是坏事,但是请谨慎选择你的风格 - 由于 Javascript 中的 自动分号插入,它可能会导致意外的 bug。 - GregL

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接