【问题标题】:Why recursive async functions in Javascript lead to stack overflow?为什么 Javascript 中的递归异步函数会导致堆栈溢出?
【发布时间】:2019-11-14 16:02:13
【问题描述】:

考虑一下这个sn-p:

function f() {
  return new Promise((resolve, reject) => {
    f().then(() => {
      resolve();
    });
  });
}

f();

也可以这样写:

async function f() {
  return await f();
}

f();

如果您运行给定的两个代码中的任何一个,您将面临以下错误:

(node:23197) UnhandledPromiseRejectionWarning: RangeError: Maximum call stack size exceeded

我的问题是为什么?在回答我的问题之前,请考虑一下我的论点:

我了解递归的概念以及在没有停止条件的情况下它如何导致堆栈溢出。但我在这里的论点是,一旦第一个 f(); 被执行,它将返回一个 Promise 并退出堆栈,所以这个递归不应该面临任何堆栈溢出。对我来说,这应该与以下行为相同:

while (1) {}

当然,如果我这样写,就修好了:

function f() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      f().then(() => {
        resolve();
      });
    }, 0);
  });
}

f();

这是一个不同的故事,我对此没有问题。

[更新]

我的错,我忘了提到我在服务器端使用node v8.10.0 进行测试。

【问题讨论】:

  • 您正在寻找Promise.resolve().then(f) 或await; return f()。目前,您的递归调用正在立即发生。记住the new Promise executor callback is synchronous。
  • 感谢@Bergi,但您的第二个建议是语法错误。
  • await undefined; return f() 然后 :-)

标签: javascript recursion promise async-await stack-overflow


【解决方案1】:

您为什么不期望它会导致无限递归? Promise 的构造函数递归地调用 f,因此 Promise 永远不会被构造,因为在构造 Promise 之前会发生无限递归循环。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

从上面的链接

executor 函数由 Promise 实现立即执行,传递 resolve 和 reject 函数(在 Promise 构造函数甚至返回创建的对象之前调用 executor)。

【讨论】:

  • 我认为你是对的,但有什么办法可以写这个不面对问题?
  • 在构建承诺之前不调用自身。
【解决方案2】:

感谢@Adrian,我设法找到了不面对堆栈溢出的方法。但在此之前,他是对的,这种形式的递归应该会导致堆栈溢出。由于问题是“为什么”,他的答案是公认的。这是我“如何”不面对堆栈溢出的尝试。

测试 1

function f() {
  return new Promise((resolve) => {
    resolve();
  }).then(f);
}

并使用await:

测试 2

async function f() {
  return await Promise.resolve()
    .then(f);
}

我不确定在这种情况下是否可以消除Promise!

我知道我说不setTimeout,但这也是一个有趣的案例:

测试 3

async function f() {
  await new Promise(resolve => setTimeout(resolve, 0));
  return f();
}

这也不会面临堆栈溢出。

最后,给你一个背景,为什么我对此感兴趣;假设您正在编写一个函数来从 AWS 的 DynamoDb 中检索所有记录。由于您可以通过一个请求从 DynamoDb 中提取多少记录是有限制的,因此您必须根据需要发送尽可能多的记录(使用ExclusiveStartKey)才能获取所有记录:

测试 4

async function getAllRecords(records = [], ExclusiveStartKey = undefined) {
    let params = {
        TableName: 'SomeTable',
        ExclusiveStartKey,
    };

    const data = await docClient.scan(params).promise();
    if (typeof data.LastEvaluatedKey !== "undefined") {
        return getAllRecords(records.concat(data.Items), data.LastEvaluatedKey);
    }
    else {
        return records.concat(data.Items);
    }
}

我想确保这永远不会面临堆栈溢出。拥有如此庞大的 DynamoDb 表来实际测试它是不可行的。所以我想出了一些例子来确保这一点。

起初,测试#4 似乎实际上可能面临堆栈溢出,但我的测试#3 表明不存在这种可能性(因为await docClient.scan(params).promise())。

[更新]

感谢@Bergi,这是await 的代码,没有Promise:

测试 5

async function f() {
  await undefined;
  return f();
}

【讨论】:

    猜你喜欢
    • 2019-10-05
    • 2016-02-28
    • 1970-01-01
    • 2011-02-26
    • 2013-04-05
    • 2021-04-19
    相关资源
    最近更新 更多