ivinnic
@ivinnic
Full-Stack - подустал

Как работает сборщик мусор с колбеками Promise?

Я передаю в промис функцию стрелку в которой используются переменные контекста.
Если этот then не выполниться, то связи переменными контекста остаются, верно?
Ведь когда этот промис выполнит resolve не известно.

Как тогда сборщик мусора понимает, что колблек со всем переменными контекста уже не используется... и можно сделать зачистку?

Пример.
Скажем был отправлен запрос на сервер, но потом компонент в котором был объявлен промис и колбек был удален.
И после удаления приходит ответ от сервера, и он выполнит колбек. Это значит что колбек остался в памяти со всеми переменными контекста. Повторяя вопрос, как же сборщик мусора поймет, что все переменные контекста можно удалить...

Пример компонента...
# vue component

props:['income_params']
data(){
  return {
    config:{...}
 }
}
mounted(){
   this.loadData()
}

methods:{
  loadData(){
     loader('url').then((data)=>{
       doSomething(data, this.config, this.income_params)
     })
  }
}


Спасибо за внимание!
Буду очень рад ответам!
  • Вопрос задан
  • 386 просмотров
Решения вопроса 1
bingo347
@bingo347 Куратор тега JavaScript
Crazy on performance...
Сборщики мусора (далее GC) бывают разные, в том же v8 используется сразу 3 типа GC в зависимости от поколения объектов (упрощенно молодое, старое и сложные случаи), но в большинстве своем принцип работы сводится к просчету достижимости из некоторого корня в дереве объектов (например глобального объекта, но не только его). v8 не является исключением, и его GC содержит C++ api для создания таких корней. Из JS мы данным api можем воспользоваться лишь косвенно, через сущности языка предоставляемые либо самим v8 либо платформой (браузером, node.js, deno и т.д.)
Чтоб было понятно давайте рассмотрим простой пример:
const h = 'Hello world!';
const n = 'nothing';
setTimeout(() => {
  console.log(h);
}, 1000);
У нас есть строковые переменные h и n. Переменная n нигде больше не используется и ее память очистится при ближайшей работе GC. А вот переменная h оказалась в замыкании созданном стрелочной функцией. И хотя в JS мы не можем достучаться до h имея ссылку на эту функцию, сама функция все таки имеет ссылку на h, а значит h не может быть уничтожена пока не будет уничтожена сама функция. В терминах GC ссылка на h будет серой, то есть сама ссылка на h недоступна из корня напрямую, но сейчас мы проверяем объекты, которые на нее ссылаются и истина будет зависеть от них (подробнее можете погуглить "mark black white and gray memory in gc").
Давайте посмотрим на саму стрелочную функцию, которая держит h в замыкании. Из кода видно, что мы ее передаем в функцию setTimeout, о которой известно, что это api предоставленное платформой (а значит вероятно какая-то часть написана на C++), а так же, что она асинхронная. Платформе реализующей setTimeout наша функция понадобится после асинхронного ожидания и никто платформе не сможет гарантировать, что во время этого ожидания не будет работы GC, поэтому ей ничего не остается, кроме как запросить у v8 создание нового корневого дерева объектов, в которое и будет положена ссылка на данную функцию.
После же выполнения таймаута платформе больше не нужна наша функция, поэтому ссылка на нее будет удалена из дерева объектов. А так как других ссылок на функцию нет, и она больше не доступна ни из одного корня - GC удалит из памяти и функцию и строку связанную h, которая так же стала недоступна из корня.

Посмотрим на пример из вопроса. У нас есть стрелочная функция, которая удерживает на себе инстанс компонента через this ссылку (так как стрелочные функции замыкают this). Саму функцию в памяти удерживает промис порожденный вызовом loader('url'), так как мы отдали её в метод then. Других ссылок на данную функцию нет, а значит и сама функция и ее замыкание (инстанс компонента) будут "жить" не менее "жизни" промиса.
Скажем был отправлен запрос на сервер, но потом компонент в котором был объявлен промис и колбек был удален.
И после удаления приходит ответ от сервера, и он выполнит колбек. Это значит что колбек остался в памяти со всеми переменными контекста
Если других ссылок не осталось, то инстанс компонента будет удерживаться от очистки через промис.

Теперь стоит разобраться с самим промисом. У него может быть 3 состояния - pending, resolved или rejected. После перехода в состояния resolved или rejected промис может выполнить сохраненные колбэки в ближайшем микротаске, а после он удалит на них ссылки из себя, в следствии чего, память удерживаемая замыканием колбэка может быть очищена (при отсутствии на нее других ссылок, достижимых из какого-либо корня).
В состоянии pending промис может потенциально находится бесконечно долго, при этом ссылаясь на все колбэки переданные ему в методы then, catch или finally, а значит так же косвенно ссылаясь на их замыкания. И тут все зависит от того, кто ссылается на данный промис, и достижим ли он из корня. И да, промис вполне может быть удален из памяти так и не дождавшись своего завершения.
интересное умозаключение
Если Promise - это обещание, то в данном случае оно будет нарушено?


В комментах к вопросу есть еще один интересный пример:
function getSomething(){
  return new Promise((resolve, reject)=>{
    if(sys_condition){
       resolve();
    } 
  })
}

function testPromise(){
  let config = {....}
  getSomething().then(()=>{
     #use config
     goOn(...config)
  })
}

testPromise();
У нас есть вызванная 1 раз функция testPromise, которая получает из функции getSomething промис, в который с помощью метода then сохраняет колбэк, удерживающий в замыкании переменную config. Сам промис она нигде не сохраняет, что здесь очень важно.
В функции getSomething мы просто возвращаем промис созданный через его конструктор, который как мы уже знаем нигде больше не сохраняется. И на этом могло бы все и закончится, без вызова колбэка независимо ни от чего. Но конструктор промиса выполняет свой колбэк синхронно, а кроме того он передает в него 2 функции - resolve и reject, которые в своем замыкании ссылаются на только что созданный промис (а это уже 2 ссылки на него, хотя мы то его никуда не сохраняли). Переменная reject никак не используется, а значит спокойно может быть удалена после завершения колбэка. Переменная resolve просто вызывается как функция внутри условия, но более тоже никак не используется и никуда не сохраняется, а значит так же может быть удалена.
В этом примере. если sys_condition = false и resolve не вызовется, это значит что создается утечка памяти
Нет, утечки памяти не будет. Колбэк созданный в testPromise будет удален вместе с замыканием, так и не вызвавшись ни разу.
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
profesor08
@profesor08 Куратор тега JavaScript
Задайся вопросом что такое промис и все встанет на свои места. На крайняк сделай console.log и посмотри на него. Промис будет ждать пока не зарезолвится, а значит он будет висеть в памяти. Что касается зависимостей в передаваемой функции в промис, то там все так-же как и с обычными функциями - подчистится все на что нет ссылок.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы