JS承诺#3:垃圾收集和内存泄漏
#javascript #编程 #gc #promise

JavaScript中的承诺是处理异步操作的一种方式。异步操作中最棘手的部分之一是,我们可能会意外地有记忆泄漏,因此,了解何时诺言以及阻止承诺被收集的承诺非常重要。

tl&drs

  1. This codesandbox具有所有诺言的例子,然后是链条。打开沙箱并重新加载。您可以首先运行Promise example,然后then example

  2. 在文章的末尾,您会从文章中找到关键发现。


有关工具的一些注释,我们将使用
本文适用于高级用户,它研究了GC(垃圾收集者)如何与承诺合作。如果您想获得有关承诺的基本信息,请查看该系列中的早期文章。

我们将使用FinalizationRegistry查找元素GCED何时。如果您不熟悉FinalizationRegistry,请检查this article
为了加快GC的速度,我们将创建大量的模拟对象,并删除它们的强大参考。

请不要在开发工具控制台中运行实验。开发工具可以使所有对象都活着,因此您将获得错误的阴性结果。

cinalizationRegistry在控制台和时间支出中打印。

重要的是要牢记,如果承诺有资格获得GC,这并不意味着它将立即收集。 JavaScript中的GC过程不是确定性的,并且由JavaScript运行时酌情发生。

让我们起草可能的方案:

明确保留对承诺的参考:

const promise = new Promise((resolve, reject) => {...});

在这种情况下,我们对承诺有很大的提及,只要const promise存在,我们就不会GC。

ð只要您明确提及自己的诺言,它就不会被

失去对承诺,解决和拒绝职能的参考:

let promiseWithoutResolve = new Promise((resolve) => {
  setTimeout(() => {
    console.log("Timeout for the promise that keeps no refs");
  }, 100000);
});

finalizationRegistry.register(promiseWithoutResolve, " which keeps no references"); 
promiseWithoutResolve = null;

对于该测试,我们既不保留强大的ref to the promise,也不保留resolve函数,但是,Promist功能具有很长的超时操作。

ð当您将所有参考丢失到resolvereject和实例本身时,对象将标记为GC,一旦GC启动,就会收集它。 (注意:JS GC有几代人,因此,如果您的承诺是第三代,则可能不会收集)。

cache resolve和/或reject方法没有强大的参考本身

在实际代码库中,您可以找到类似的东西:

let resolve;
let promise = new Promise((_resolve) => { 
  resolve = _resolve;
});
// Let's remove the reference to promise
promise = null;

此代码执行2件事:
1)删除对承诺的有力参考
2)caches resolve在承诺构造函数中的回调外函数。

我们不再可以直接访问这一承诺,但是目前尚不清楚该诺言是否会排队为GC排队,或者只要我们将链接保留到resolve

要测试这种行为,我们可以起草一个实验:

let promiseWithResolve = new Promise((resolve) => {
  setTimeout(() => {
    resolve();
  }, 100000);
});

finalizationRegistry.register(promiseWithResolve , " which keeps resolve function"); 
promiseWithResolve = null;

对于这样的实验,我们有一个输出:

Promise  which keeps resolve function. Time taken: 155765. 

ð只要我们提到resolvereject回调,承诺就会保持在记忆中。
ð如果我们有一个长期生活的诺言,它可能会到达老一辈,这意味着在失去所有裁判并获得诺言之间需要更多的时间。

.then链接

日常用例:

new Promise(() => {}).then(() => {/* Some async code */}) 

测试的实验:

let promiseWithThen = new Promise(() => {});
let then = promiseWithThen.then(() => {
  console.log("then reached");
});

finalizationRegistry.register(promiseWithThen, " with `then` chain");
finalizationRegistry.register(then, " then callback");

promiseWithThen = then = null;

输出:

Promise  with `then` chain. Time taken: 191. 
Promise  then callback. Time taken: 732. 

原始的promiseWithThen将永远无法解决,但是它是通过遵循then操作链接的,这可能会保留对原始承诺的参考。幸运的是,then并不能阻止承诺被淘汰。

ð.then不能阻止GC的承诺。仅明确提及承诺自己,resolvereject很重要。

如果我们将.then添加到这些实验中会发生什么?

正如我们发现的,.then。不能阻止垃圾收集的承诺。这意味着,它不应有任何效果。

为了证明这一点,我们可以在此沙盒中起草Then example下的实验:https://codesandbox.io/s/promises-article-first-example-8jfyh?file=/src/index.js

运行实验时,您会看到then确实没有效果,并且不会改变任何行为。

为什么几个。然后连锁继续保证?

有时在代码中,我们有链条:

Promise.resolve()
  .then(asyncCode1)
  .then(asyncCode2)
  ...
  .then(asyncCodeN);

即使我们不遵守承诺,也没有计划在链条完成之前为GC。
问题是:Promise.resolve()返回了解决的承诺,并且第一个.then计划运行一个微型,因为以前的承诺已解决。因此,我们将有一个计划执行的范围。

.then返回了由以下异步操作(asyncCode2,...)链接的承诺。异步操作的值是以前的.then块的返回值(在我们的情况下,是fn asyncCode1)。

ð如果您的.then链获得“控制”(计划执行甚至启动执行),则该功能范围具有解决或拒绝.then返回的承诺的参考,因此该诺言不会是ged。

总结:

关键发现的简短列表:

ð引用:promise instance本身,resolvereject遵守垃圾收集的承诺。

ð.then不能阻止GC的承诺。

ð如果您的.then链获得“控制”(计划执行甚至启动执行),则该功能范围具有解决或拒绝.then返回的承诺的参考,因此该诺言不会是ged。

ð如果我们有一个长期生活的诺言,它可能会到达老一辈,这意味着在失去所有裁判和诺言之间需要更多的时间。<<<<<<<<< /p>