为什么 Javascript 中的递归异步函数会导致堆栈溢出?
Why recursive async functions in Javascript lead to 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();
被执行,它将 return 一个 Promise
并且它退出堆栈,所以这个递归不应该面临任何堆栈溢出。对我来说,这应该与以下行为相同:
while (1) {}
当然,如果我这样写,就搞定了:
function f() {
return new Promise((resolve, reject) => {
setTimeout(() => {
f().then(() => {
resolve();
});
}, 0);
});
}
f();
这是一个不同的故事,我没有问题。
[更新]
不好意思,我忘了说我是在服务器端用 node v8.10.0
测试的。
为什么你不认为它会导致无限递归? promise 的构造函数正在递归调用 f,因此 promise 永远不会构造,因为在构造 promise 之前会发生无限递归循环。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
来自上面的link
执行器函数由 Promise 实现立即执行,传递 resolve 和 reject 函数(执行器在 Promise 构造函数之前调用,甚至 returns 创建的对象)。
感谢@Adrian,我设法找到了一种不面临堆栈溢出的方法。但在此之前,他是对的,递归的形式应该会导致堆栈溢出。由于问题是"why",他的答案是公认的。这是我在 "how" 上不面对堆栈溢出的尝试。
测试 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 table 来实际测试它是不可行的。所以我想出了一些例子来确保这一点。
起初,似乎测试 #4 实际上可能面临堆栈溢出,但我的测试 #3 表明没有这种可能性(因为 await docClient.scan(params).promise()
)。
[更新]
感谢@Bergi,这是 await
没有 Promise
的代码:
测试 5
async function f() {
await undefined;
return f();
}
考虑这个片段:
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();
被执行,它将 return 一个 Promise
并且它退出堆栈,所以这个递归不应该面临任何堆栈溢出。对我来说,这应该与以下行为相同:
while (1) {}
当然,如果我这样写,就搞定了:
function f() {
return new Promise((resolve, reject) => {
setTimeout(() => {
f().then(() => {
resolve();
});
}, 0);
});
}
f();
这是一个不同的故事,我没有问题。
[更新]
不好意思,我忘了说我是在服务器端用 node v8.10.0
测试的。
为什么你不认为它会导致无限递归? promise 的构造函数正在递归调用 f,因此 promise 永远不会构造,因为在构造 promise 之前会发生无限递归循环。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
来自上面的link
执行器函数由 Promise 实现立即执行,传递 resolve 和 reject 函数(执行器在 Promise 构造函数之前调用,甚至 returns 创建的对象)。
感谢@Adrian,我设法找到了一种不面临堆栈溢出的方法。但在此之前,他是对的,递归的形式应该会导致堆栈溢出。由于问题是"why",他的答案是公认的。这是我在 "how" 上不面对堆栈溢出的尝试。
测试 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 table 来实际测试它是不可行的。所以我想出了一些例子来确保这一点。
起初,似乎测试 #4 实际上可能面临堆栈溢出,但我的测试 #3 表明没有这种可能性(因为 await docClient.scan(params).promise()
)。
[更新]
感谢@Bergi,这是 await
没有 Promise
的代码:
测试 5
async function f() {
await undefined;
return f();
}