【问题标题】:How can I wait for a recursive function to completely finish before continuing?如何在继续之前等待递归函数完全完成?
【发布时间】:2021-06-26 20:57:20
【问题描述】:

我有一个看起来像这样的函数:

async function convertExamples(data,path,urlParameters) {
    urlParameters='h'
    for (var k in data) {
        if (typeof data[k] == "object" && data[k] !== null)
            return await convertExamples(data[k], path+'.'+k, urlParameters); 
        else {
            return urlParameters + path+'.'+k + '=' + data[k] + '&';
        }
    }
    return urlParameters;
}

如您所见,它是递归的(它在第 4 行自行运行)。

它所做的只是遍历一个嵌套对象以获取最后的值,并将它们添加到一个 url 字符串中。

let urlParameters = await convertExamples(data,'gui','?');
console.log('finished url parameters:',urlParameters);

现在它似乎不起作用,因为 urlParameters 仍然等于 ?最后。

我怎样才能让程序等待convertExamples() 函数完成(包括从自身内部运行的每个实例),然后再继续?

我尝试在调用它之前添加await,但似乎没有任何改变。

如果可能,我宁愿避免承诺。

【问题讨论】:

  • 哎呀,忘了你必须标记语言,javascript!

标签: javascript asynchronous recursion async-await promise


【解决方案1】:

您对异步编程和递归函数之间的区别感到困惑。您需要“等待”异步进程完成承诺解决方案、回调或异步/等待。但是这个函数是同步的,尽管它是递归的。你没有得到你期望的答案,因为你没有返回任何值。你不需要做任何额外的事情来等待它完成。获取“?”的输出表示完成了。

这是您的函数的更正版本。它在递归时跟踪路径,但不需要第三个参数。

function convertExamples(data, path)   {
    let urlParameters = '';
    for (var k in data) {
        if (typeof data[k] == "object" && data[k] !== null)
            urlParameters += convertExamples(data[k], path+'.'+k);
        else
            urlParameters += path+'.'+k + '=' + data[k] + '&';
    }
    return urlParameters;
}

const data = {
  a: {
    b: 'foo',
    c: 'bar',
  },
  d: 'baz',
};

const result = '?' + convertExamples(data,'gui')

console.log('finished url parameters:',result);

// finished url parameters: ?gui.a.b=foo&gui.a.c=bar&gui.d=baz&


编辑

那么有什么区别呢?您的递归函数使 JavaScript 进程一直处于忙碌状态。在这方面,调用自身的函数与调用彼此的不同函数链没有什么不同。在所有这些执行完成之前,JavaScript 不会也不能做任何其他事情。那是同步的。

异步编程是指 JavaScript 注册一个回调(即要执行的代码)以由未来的事件触发,然后 停止。您正在考虑的“等待”实际上是关于“我如何确保 JavaScript 在该事件发生时唤醒并再次开始执行”?该事件可能是网络调用返回,或者用户单击鼠标,或者您设置的最终与时钟匹配的超时。正如我之前所暗示的,回调、promise 和 async/await 都只是我们如何排队代码以便将来在某些事件上运行的糖。

【讨论】:

  • 这很有意义(并且有效)。我想我认为 for 循环是异步的并且是问题所在,而实际上是字符串没有正确传回。
【解决方案2】:

字符串在 javascript 中是原始且不可变的,因此您无法修改它们 - 只能替换。 urlParameters += ... 行实际上创建了urlParameters 的本地副本并对其进行处理,而不是修改原始文件。您可以从每个函数调用中返回字符串:

function convertExamples(data,path,urlParameters)   {
    for (var k in data) {
        if (typeof data[k] == "object" && data[k] !== null)
            urlParameters = convertExamples(data[k], path+'.'+k, urlParameters); 
        else 
            urlParameters += path+'.'+k + '=' + data[k] + '&';
    }
    return urlParameters;
}
...
let urlParameters = '?';
urlParameters = convertExamples(data,'gui',urlParameters)

或者更好的是,使用可修改的数据类型,例如数组,并在最后将其解析为字符串:

function convertExamples(data,path,paramsArr)   {
    for (var k in data) {
        if (typeof data[k] == "object" && data[k] !== null)
            convertExamples(data[k], path+'.'+k, paramsArr); 
        else 
            paramsArr.push(path+'.'+k + '=' + data[k]);
    }
}
...
const paramsArr = [];
convertExamples(data,'gui',paramsArr);
const urlParameters = '?' + paramsArr.join('&');

【讨论】:

    【解决方案3】:

    Chris 的帖子告诉您,这可以是一个纯粹的同步函数。我认为您应该更进一步,将程序的关注点分开。

    我们可以从对象展平函数flatten 开始。这允许我们以线性方式处理任意嵌套的对象。最重要的是,它是通用的,这意味着它可以在您需要此行为的任何时候重用 -

    function* flatten (t, path = [])
    { switch (t?.constructor)
      { case Object:
        case Array:
          for (const [k,v] of Object.entries(t))
            yield *flatten(v, [...path, k])
          break
        default:
          yield [path, t]
      }
    }
    
    const data = {
      a: {
        b: 'foo',
        c: 'bar',
      },
      d: 'baz',
    };
    
    for (const [path, value] of flatten(data))
      console.log(JSON.stringify(path), value)
    ["a","b"] foo
    ["a","c"] bar
    ["d"] baz
    

    接下来我们将编写objectToParams,它是对flatten 的简单包装。请注意调用者如何处理path,但它适合他们的需要。我们将使用URLSearchParams,而不是使用字符串连接手动将 URL 组件组合在一起。这是一个更安全的 API,并且直接与 URL api 相关联 -

    function objectToParams (t, base)
    { const p = new URLSearchParams()
      for (const [path, value] of flatten(t, base ? [ base ] : []))
        p.set(path.join("."), value)
      return p.toString()
    }
    
    console.log(objectToParams(data))
    console.log(objectToParams(data, "gui"))
    

    展开下面的sn-p,在自己的浏览器中验证结果-

    function* flatten (t, path = [])
    { switch (t?.constructor)
      { case Object:
        case Array:
          for (const [k,v] of Object.entries(t))
            yield *flatten(t[k], [...path, k])
          break
        default:
          yield [path, t]
      }
    }
    
    function objectToParams (t, base)
    { const p = new URLSearchParams()
      for (const [path, value] of flatten(t, base ? [ base ] : []))
        p.set(path.join("."), value)
      return p.toString()
    }
    
    const data = {
      a: {
        b: 'foo',
        c: 'bar',
      },
      d: 'baz',
    };
    
    console.log(objectToParams(data))
    console.log(objectToParams(data, "gui"))
    a.b=foo&a.c=bar&d=baz
    gui.a.b=foo&gui.a.c=bar&gui.d=baz
    

    【讨论】:

    • 我写了很多相同的答案,但决定不发表。我很高兴你做到了。一个问题:递归调用使用t[k]而不是v有什么原因吗?显然,这就是您使用 Object.keys 会做的事情,但使用 Object.entries 感觉很奇怪。
    • 善于观察细节,斯科特。我昨晚很晚才写了这篇文章,并打算给它更多的解释。我绝对应该使用v :D
    • “昨晚很晚”是我没有发表我的原因。 :-) 床似乎比写解释更吸引人。
    猜你喜欢
    • 1970-01-01
    • 2015-09-16
    • 2020-02-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-12
    • 2010-09-05
    • 1970-01-01
    相关资源
    最近更新 更多