为什么Javascript中的递归异步函数会导致堆栈溢出?

时间:2019-07-03 23:23:02

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

请考虑以下代码段:

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进行测试。

2 个答案:

答案 0 :(得分:5)

您为什么不希望它引起无限递归? promise的构造函数以递归方式调用f,因此promise永远都无法构造,因为在promise构建之前会发生无限递归循环。

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

从上面的链接

执行器功能由Promise实现立即执行,传递解决和拒绝功能(在Promise构造函数甚至返回创建的对象之前调用执行器)。

答案 1 :(得分:1)

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