Ожидается, но никогда не разрешается/отклоняется использование памяти обещаний

Приведет ли await к использованию Promise, который ни разрешает, ни отклоняет (никогда не устанавливает/не выполняет) утечку памяти?

Мне стало интересно об этом, когда я смотрел на перехватчики React с помощью slorber/awesome-debounce-обещания, который создает новые обещания, но выполняет только последние из них, оставляя многие/наиболее нерешенными/невыполненными.

Ответ 1

Предисловие (вы, вероятно, знаете это!):

await - синтаксический сахар для использования обещаний обратных вызовов. (Действительно, действительно, действительно хороший сахар.) Функция async - это функция, в которой движок JavaScript создает цепочки обещаний и тому подобное для вас.

Ответ:

Речь идет не столько о том, выполнено ли обещание, сколько о том, хранятся ли в памяти обратные вызовы (и то, на что они ссылаются/закрываются). Пока обещание находится в памяти и не установлено, оно имеет ссылку на свои функции обратного вызова, сохраняя их в памяти. Две вещи заставляют эти ссылки исчезнуть:

  1. Выполнение обещания или
  2. Освобождение всех ссылок на обещание, что делает его подходящим для GC (возможно, подробнее ниже)

В обычном случае потребитель обещания подключает обработчики к обещанию, а затем либо вообще не сохраняет ссылку на него, либо сохраняет ссылку на него только в контексте, в котором функции обработчика закрываются, а не где-либо еще. (Вместо того, чтобы, например, хранить ссылку на обещание в свойстве объекта с длительным сроком действия.)

Предполагая, что реализация debounce освобождает ссылку на обещание, которое никогда не будет выполнено, и потребитель обещания не сохранил ссылку где-то за пределами этого цикла взаимных ссылок, тогда обещание и обработчики, зарегистрированные для него (и все, что они содержат единственную ссылку для) могут быть собраны мусором, как только ссылка на обещание будет выпущена.

Это требует изрядного внимания со стороны реализации. Например (спасибо Киту за пометку этого), если обещание использует обратный вызов для какого-то другого API (например, addEventListener), и обратный вызов закрывается по ссылке на обещание, так как другой API содержит ссылку на обратный вызов, которая может помешать освобождению всех ссылок на обещание и, таким образом, сохранить в памяти все, на что ссылается обещание (например, его обратные вызовы).

Так что это будет зависеть от тщательности реализации и от потребителя. Можно было бы написать код, который содержал бы ссылки на обещания и, таким образом, вызывал бы утечку памяти, но в обычном случае я бы не ожидал, что это сделает потребитель.

Ответ 2

Я провел некоторое тестирование, используя следующую структуру:

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

Выполнение только результирующего фрейма в Google Chrome показывало непрерывное увеличение использования памяти на вкладке Память инструментов разработчика (но не на вкладке "Куча производительности /JS"), что указывает на утечку. Выполнение этого, но выполнение обещаний не протекало.

Запуск этого увеличенного использования памяти для меня увеличился на 1-4 МБ/с. Остановка и запуск GC не освободили ничего из этого.

Google Chrome dev tools Memory tab showing increasing usage