等待但从未 resolved/rejected 承诺内存使用
Awaited but never resolved/rejected promise memory usage
await
ing 既不解析也不拒绝(从不 settle/unfulfilled)的 Promise
会导致内存泄漏吗?
我在查看带有 slorber/awesome-debounce-promise 的 React 钩子时对此感到好奇,它创建了新的承诺,但只解决了最后一个承诺,因此 many/most unsettle/unfulfilled.
前言(你可能知道这个!):
await
是使用 promise 回调的语法糖。 (真的,真的,非常好的糖。) async
函数是 JavaScript 引擎为您构建承诺链等的函数。
答案:
相关的不是promise 是否被解决,而是promise 回调(以及它们引用/关闭的东西)是否保留在内存中。虽然 promise 在内存中并且未解决,但它有一个对其回调函数的引用,将它们保存在内存中。有两件事使这些引用消失:
- 兑现承诺,或者
- 释放对 promise 的所有引用,使其符合 GC 条件(可能,更多内容见下文)
在正常情况下,promise 的消费者将处理程序连接到 promise,然后要么根本不保留对它的引用,要么只在处理程序函数关闭的上下文中保留对它的引用结束而不是其他地方。 (而不是,例如,将 promise 引用保存在一个长期存在的对象中 属性。)
假设去抖实现释放了它对它永远不会结算的承诺的引用,并且承诺的消费者没有在这个相互引用循环之外的某个地方存储引用,那么承诺和处理程序注册到一旦对 promise 的引用被释放,它(以及它们持有唯一引用的任何东西)都可以被垃圾收集。
这需要在实施方面多加注意。例如 (感谢 Keith 将其标记出来),如果承诺使用其他 API(例如,addEventListener
)的回调,并且回调关闭对 promise 的引用,因为另一个 API 有对回调的引用,这可能会阻止释放对 promise 的所有引用,从而保留 promise 引用的任何内容(例如它的回调) 在内存中。
因此,这将取决于实施的谨慎程度,以及对消费者的影响。可以编写保留对承诺的引用的代码,从而导致内存泄漏,但在正常情况下,我不希望消费者这样做。
我使用以下结构进行了一些测试:
function doesntSettle() {
return new Promise(function(resolve, reject) {
// Never settle the promise
});
}
let awaited = 0;
let resolved = 0;
async function test() {
awaited++;
await doesntSettle();
resolved++;
}
setInterval(() => {
for (let i = 0; i < 100; ++i) {
test();
}
}, 1);
在这里实现:https://codesandbox.io/s/unsetteled-awaited-promise-memory-usage-u44oc
运行 just the result frame in Google Chrome 显示开发工具内存选项卡中的内存使用量不断增加(但不在 Performance/JS 堆选项卡下),表明泄漏。 运行这可是解决了承诺没有泄露。
运行 这增加了我的内存使用量,增加了 1-4MB/秒。停止它并且 运行 GC 没有释放任何它。
await
ing 既不解析也不拒绝(从不 settle/unfulfilled)的 Promise
会导致内存泄漏吗?
我在查看带有 slorber/awesome-debounce-promise 的 React 钩子时对此感到好奇,它创建了新的承诺,但只解决了最后一个承诺,因此 many/most unsettle/unfulfilled.
前言(你可能知道这个!):
await
是使用 promise 回调的语法糖。 (真的,真的,非常好的糖。) async
函数是 JavaScript 引擎为您构建承诺链等的函数。
答案:
相关的不是promise 是否被解决,而是promise 回调(以及它们引用/关闭的东西)是否保留在内存中。虽然 promise 在内存中并且未解决,但它有一个对其回调函数的引用,将它们保存在内存中。有两件事使这些引用消失:
- 兑现承诺,或者
- 释放对 promise 的所有引用,使其符合 GC 条件(可能,更多内容见下文)
在正常情况下,promise 的消费者将处理程序连接到 promise,然后要么根本不保留对它的引用,要么只在处理程序函数关闭的上下文中保留对它的引用结束而不是其他地方。 (而不是,例如,将 promise 引用保存在一个长期存在的对象中 属性。)
假设去抖实现释放了它对它永远不会结算的承诺的引用,并且承诺的消费者没有在这个相互引用循环之外的某个地方存储引用,那么承诺和处理程序注册到一旦对 promise 的引用被释放,它(以及它们持有唯一引用的任何东西)都可以被垃圾收集。
这需要在实施方面多加注意。例如 (感谢 Keith 将其标记出来),如果承诺使用其他 API(例如,addEventListener
)的回调,并且回调关闭对 promise 的引用,因为另一个 API 有对回调的引用,这可能会阻止释放对 promise 的所有引用,从而保留 promise 引用的任何内容(例如它的回调) 在内存中。
因此,这将取决于实施的谨慎程度,以及对消费者的影响。可以编写保留对承诺的引用的代码,从而导致内存泄漏,但在正常情况下,我不希望消费者这样做。
我使用以下结构进行了一些测试:
function doesntSettle() {
return new Promise(function(resolve, reject) {
// Never settle the promise
});
}
let awaited = 0;
let resolved = 0;
async function test() {
awaited++;
await doesntSettle();
resolved++;
}
setInterval(() => {
for (let i = 0; i < 100; ++i) {
test();
}
}, 1);
在这里实现:https://codesandbox.io/s/unsetteled-awaited-promise-memory-usage-u44oc
运行 just the result frame in Google Chrome 显示开发工具内存选项卡中的内存使用量不断增加(但不在 Performance/JS 堆选项卡下),表明泄漏。 运行这可是解决了承诺没有泄露。
运行 这增加了我的内存使用量,增加了 1-4MB/秒。停止它并且 运行 GC 没有释放任何它。