Recursos “ocultos” da linguagem
Este artigo cobre um tópico com foco muito restrito, que a maioria dos desenvolvedores raramente encontra na prática (e podem nem estar cientes de sua existência).
Recomendamos pular este capítulo se você acabou de começar a aprender JavaScript.
Relembrando o conceito básico do princípio de acessibilidade do capítulo Coleta de lixo, podemos notar que o mecanismo JavaScript tem a garantia de manter na memória valores que estão acessíveis ou em uso.
Por exemplo:
// a variável do usuário contém uma referência forte ao objeto deixe usuário = {nome: "John" }; // vamos sobrescrever o valor da variável do usuário usuário = nulo; // a referência é perdida e o objeto será deletado da memória
Ou um código semelhante, mas um pouco mais complicado, com duas referências fortes:
// a variável do usuário contém uma referência forte ao objeto deixe usuário = {nome: "John" }; // copiou a referência forte ao objeto na variável admin deixe admin = usuário; // vamos sobrescrever o valor da variável do usuário usuário = nulo; // o objeto ainda pode ser acessado através da variável admin
O objeto { name: "John" }
só seria deletado da memória se não houvesse referências fortes a ele (se também sobrescrevêssemos o valor da variável admin
).
Em JavaScript, existe um conceito chamado WeakRef
, que se comporta de maneira um pouco diferente neste caso.
Termos: “Referência forte”, “Referência fraca”
Referência forte – é uma referência a um objeto ou valor, que evita que sejam excluídos pelo coletor de lixo. Mantendo assim na memória o objeto ou valor para o qual aponta.
Isso significa que o objeto ou valor permanece na memória e não é coletado pelo coletor de lixo enquanto houver referências fortes ativas a ele.
Em JavaScript, referências comuns a objetos são referências fortes. Por exemplo:
// a variável do usuário contém uma referência forte para este objeto deixe usuário = {nome: "John" };
Referência fraca – é uma referência a um objeto ou valor, que não impede que sejam deletados pelo coletor de lixo. Um objeto ou valor pode ser excluído pelo coletor de lixo se as únicas referências restantes a eles forem referências fracas.
Nota de cautela
Antes de nos aprofundarmos nisso, é importante notar que o uso correto das estruturas discutidas neste artigo requer uma reflexão muito cuidadosa e é melhor evitá-las, se possível.
WeakRef
– é um objeto que contém uma referência fraca a outro objeto, denominado target
ou referent
.
A peculiaridade do WeakRef
é que ele não impede que o coletor de lixo exclua seu objeto referente. Em outras palavras, um objeto WeakRef
não mantém o objeto referent
ativo.
Agora vamos tomar a variável user
como “referente” e criar uma referência fraca dela para a variável admin
. Para criar uma referência fraca, você precisa usar o construtor WeakRef
, passando o objeto de destino (o objeto para o qual deseja uma referência fraca).
No nosso caso — esta é a variável user
:
// a variável do usuário contém uma referência forte ao objeto deixe usuário = {nome: "John" }; // a variável admin contém uma referência fraca ao objeto deixe admin = novo WeakRef(usuário);
O diagrama abaixo descreve dois tipos de referências: uma referência forte usando a variável user
e uma referência fraca usando a variável admin
:
Então, em algum momento, paramos de usar a variável user
– ela é sobrescrita, sai do escopo, etc., enquanto mantemos a instância WeakRef
na variável admin
:
// vamos sobrescrever o valor da variável do usuário usuário = nulo;
Uma referência fraca a um objeto não é suficiente para mantê-lo “vivo”. Quando as únicas referências restantes a um objeto referente são referências fracas, o coletor de lixo fica livre para destruir esse objeto e usar sua memória para outra coisa.
Porém, até que o objeto seja realmente destruído, a referência fraca poderá devolvê-lo, mesmo que não haja mais referências fortes a esse objeto. Ou seja, nosso objeto se torna uma espécie de “gato de Schrödinger” – não podemos saber ao certo se ele está “vivo” ou “morto”:
Neste ponto, para obter o objeto da instância WeakRef
, usaremos seu método deref()
.
O método deref()
retorna o objeto referente para o qual o WeakRef
aponta, se o objeto ainda estiver na memória. Se o objeto foi excluído pelo coletor de lixo, o método deref()
retornará undefined
:
deixe ref = admin.deref(); se (ref.) { // o objeto ainda está acessível: podemos realizar qualquer manipulação com ele } outro { // o objeto foi coletado pelo coletor de lixo }
WeakRef
é normalmente usado para criar caches ou matrizes associativas que armazenam objetos que consomem muitos recursos. Isso permite evitar que esses objetos sejam coletados pelo coletor de lixo apenas com base em sua presença no cache ou na matriz associativa.
Um dos principais exemplos é uma situação em que temos vários objetos de imagem binária (por exemplo, representados como ArrayBuffer
ou Blob
) e queremos associar um nome ou caminho a cada imagem. As estruturas de dados existentes não são muito adequadas para estes fins:
Utilizar Map
para criar associações entre nomes e imagens, ou vice-versa, manterá os objetos de imagem na memória, pois estão presentes no Map
como chaves ou valores.
WeakMap
também não é elegível para este objetivo: porque os objetos representados como chaves WeakMap
usam referências fracas e não são protegidos contra exclusão pelo coletor de lixo.
Mas, nesta situação, precisamos de uma estrutura de dados que utilize referências fracas em seus valores.
Para isso, podemos utilizar uma coleção Map
, cujos valores são instâncias WeakRef
referentes aos objetos grandes que necessitamos. Conseqüentemente, não manteremos esses objetos grandes e desnecessários na memória por mais tempo do que deveriam.
Caso contrário, esta é uma forma de obter o objeto de imagem do cache, se ainda estiver acessível. Se tiver sido coletado como lixo, iremos gerá-lo novamente ou baixá-lo novamente.
Dessa forma, menos memória é utilizada em algumas situações.
Abaixo está um trecho de código que demonstra a técnica de uso de WeakRef
.
Resumindo, usamos um Map
com chaves de string e objetos WeakRef
como seus valores. Se o objeto WeakRef
não foi coletado pelo coletor de lixo, nós o obtemos do cache. Caso contrário, baixamos novamente e colocamos no cache para possível reutilização:
function buscarImg() { //função abstrata para download de imagens... } function fracoRefCache(fetchImg) { // (1) const imgCache = novo Mapa(); // (2) return (imgNome) => { // (3) const cachedImg = imgCache.get(imgNome); // (4) if (cachedImg?.deref()) { // (5) retornar cachedImg?.deref(); } const newImg = fetchImg(imgNome); // (6) imgCache.set(imgName, new WeakRef(newImg)); // (7) retornar novoImg; }; } const getCachedImg=fracoRefCache(fetchImg);
Vamos nos aprofundar nos detalhes do que aconteceu aqui:
weakRefCache
– é uma função de ordem superior que usa outra função, fetchImg
, como argumento. Neste exemplo, podemos negligenciar uma descrição detalhada da função fetchImg
, pois pode ser qualquer lógica de download de imagens.
imgCache
– é um cache de imagens, que armazena resultados em cache da função fetchImg
, na forma de chaves de string (nome da imagem) e objetos WeakRef
como seus valores.
Retorna uma função anônima que usa o nome da imagem como argumento. Este argumento será usado como chave para a imagem em cache.
Tentando obter o resultado armazenado em cache do cache, usando a chave fornecida (nome da imagem).
Se o cache contiver um valor para a chave especificada e o objeto WeakRef
não tiver sido excluído pelo coletor de lixo, retorne o resultado armazenado em cache.
Se não houver nenhuma entrada no cache com a chave solicitada, ou o método deref()
retornar undefined
(significando que o objeto WeakRef
foi coletado como lixo), a função fetchImg
baixa a imagem novamente.
Coloque a imagem baixada no cache como um objeto WeakRef
.
Agora temos uma coleção Map
, onde as chaves – são nomes de imagens como strings, e valores – são objetos WeakRef
contendo as próprias imagens.
Essa técnica ajuda a evitar a alocação de uma grande quantidade de memória para objetos que consomem muitos recursos, que ninguém mais usa. Também economiza memória e tempo no caso de reutilização de objetos em cache.
Aqui está uma representação visual da aparência deste código:
Mas, esta implementação tem suas desvantagens: com o tempo, Map
será preenchido com strings como chaves, que apontam para um WeakRef
, cujo objeto-referente já foi coletado como lixo:
Uma maneira de lidar com esse problema é limpar periodicamente o cache e limpar as entradas “mortas”. Outra forma – é usar finalizadores, que exploraremos a seguir.
Outro caso de uso do WeakRef
é o rastreamento de objetos DOM.
Vamos imaginar um cenário onde algum código ou biblioteca de terceiros interage com elementos da nossa página, desde que existam no DOM. Por exemplo, poderia ser um utilitário externo para monitorar e notificar sobre o estado do sistema (comumente chamado de “logger” – um programa que envia mensagens informativas chamadas “logs”).
Exemplo interativo:
Resultado
index.js
índice.css
index.html
const startMessagesBtn = document.querySelector('.start-messages'); // (1) const closeWindowBtn = document.querySelector('.window__button'); // (2) const windowElementRef = new WeakRef(document.querySelector(".window__body")); // (3) startMessagesBtn.addEventListener('click', () => { // (4) startMessages(windowElementRef); startMessagesBtn.disabled = verdadeiro; }); closeWindowBtn.addEventListener('clique', () => document.querySelector(".window__body").remove()); // (5) const startMessages = (elemento) => { const timerId = setInterval(() => { // (6) if (element.deref()) { // (7) carga útil const = document.createElement("p"); payload.textContent = `Mensagem: Status do sistema OK: ${new Date().toLocaleTimeString()}`; element.deref().append(carga útil); } senão { // (8) alert("O elemento foi excluído."); // (9) clearInterval(timerId); } }, 1000); };
.aplicativo { exibição: flexível; direção flexível: coluna; lacuna: 16px; } .mensagens iniciais { largura: conteúdo adequado; } .janela { largura: 100%; borda: 2px sólido #464154; estouro: oculto; } .window__header { posição: pegajosa; preenchimento: 8px; exibição: flexível; justificar-conteúdo: espaço entre; alinhar itens: centro; cor de fundo: #736e7e; } .window__title { margem: 0; tamanho da fonte: 24px; peso da fonte: 700; cor: branco; espaçamento entre letras: 1px; } .window__button { preenchimento: 4px; plano de fundo: #4f495c; esboço: nenhum; borda: 2px sólido #464154; cor: branco; tamanho da fonte: 16px; cursor: ponteiro; } .window__body { altura: 250px; preenchimento: 16px; estouro: rolar; cor de fundo: #736e7e33; }
<!DOCTYPEHTML> <html lang="pt"> <cabeça> <meta charset="utf-8"> <link rel="stylesheet" href="index.css"> <title>Logger DOM WeakRef</title> </head> <corpo> <div class="aplicativo"> <button class="start-messages">Começar a enviar mensagens</button> <div class="janela"> <div class="window__header"> <p class="window__title">Mensagens:</p> <button class="window__button">Fechar</button> </div> <div class="janela__body"> Nenhuma mensagem. </div> </div> </div> <script type="module" src="index.js"></script> </body> </html>
Ao clicar no botão “Iniciar envio de mensagens”, na chamada “janela de exibição de logs” (elemento da classe .window__body
), mensagens (logs) começam a aparecer.
Mas, assim que este elemento for excluído do DOM, o logger deverá parar de enviar mensagens. Para reproduzir a remoção deste elemento, basta clicar no botão “Fechar” no canto superior direito.
Para não complicar nosso trabalho e não notificar código de terceiros toda vez que nosso elemento DOM estiver disponível, e quando não estiver, será suficiente criar uma referência fraca a ele usando WeakRef
.
Assim que o elemento for removido do DOM, o logger irá notá-lo e parar de enviar mensagens.
Agora vamos dar uma olhada mais de perto no código-fonte ( tab index.js
):
Obtenha o elemento DOM do botão “Começar a enviar mensagens”.
Obtenha o elemento DOM do botão “Fechar”.
Obtenha o elemento DOM da janela de exibição de logs usando o new WeakRef()
. Dessa forma, a variável windowElementRef
mantém uma referência fraca ao elemento DOM.
Adicione um event listener no botão “Iniciar envio de mensagens”, responsável por iniciar o logger quando clicado.
Adicione um event listener no botão “Fechar”, responsável por fechar a janela de exibição de logs quando clicado.
Use setInterval
para começar a exibir uma nova mensagem a cada segundo.
Se o elemento DOM da janela de exibição de logs ainda estiver acessível e mantido na memória, crie e envie uma nova mensagem.
Se o método deref()
retornar undefined
, significa que o elemento DOM foi excluído da memória. Neste caso, o registrador para de exibir mensagens e limpa o cronômetro.
alert
, que será chamado após o elemento DOM da janela de exibição de logs ser excluído da memória (ou seja, após clicar no botão “Fechar”). Observe que a exclusão da memória pode não acontecer imediatamente, pois depende apenas dos mecanismos internos do coletor de lixo.
Não podemos controlar esse processo diretamente do código. Porém, apesar disso, ainda temos a opção de forçar a coleta de lixo do navegador.
No Google Chrome, por exemplo, para fazer isso, você precisa abrir as ferramentas do desenvolvedor ( Ctrl + Shift + J no Windows/Linux ou Option + ⌘ + J no macOS), ir até a aba “Desempenho” e clicar no botão botão do ícone da lixeira – “Coletar lixo”:
Esta funcionalidade é suportada na maioria dos navegadores modernos. Após as ações serem tomadas, o alert
será acionado imediatamente.
Agora é hora de falar sobre finalizadores. Antes de prosseguirmos, vamos esclarecer a terminologia:
Callback de limpeza (finalizador) - é uma função que é executada quando um objeto, registrado no FinalizationRegistry
, é excluído da memória pelo coletor de lixo.
Sua finalidade – é fornecer a capacidade de realizar operações adicionais, relacionadas ao objeto, após ele ter sido finalmente excluído da memória.
Registry (ou FinalizationRegistry
) – é um objeto especial em JavaScript que gerencia o registro e cancelamento de registro de objetos e seus retornos de chamada de limpeza.
Este mecanismo permite registrar um objeto para rastrear e associar um retorno de chamada de limpeza a ele. Essencialmente, é uma estrutura que armazena informações sobre objetos registrados e seus retornos de chamada de limpeza e, em seguida, invoca automaticamente esses retornos de chamada quando os objetos são excluídos da memória.
Para criar uma instância do FinalizationRegistry
, ele precisa chamar seu construtor, que recebe um único argumento – o retorno de chamada de limpeza (finalizador).
Sintaxe:
function limpezaCallback(holdValue) { //limpa o código de retorno de chamada } registro const = novo FinalizationRegistry(cleanupCallback);
Aqui:
cleanupCallback
– um retorno de chamada de limpeza que será chamado automaticamente quando um objeto registrado for excluído da memória.
heldValue
– o valor que é passado como argumento para o retorno de chamada de limpeza. Se heldValue
for um objeto, o registro manterá uma referência forte a ele.
registry
– uma instância de FinalizationRegistry
.
Métodos de FinalizationRegistry
:
register(target, heldValue [, unregisterToken])
– usado para registrar objetos no registro.
target
– o objeto que está sendo registrado para rastreamento. Se o target
for coletado como lixo, o retorno de chamada de limpeza será chamado heldValue
como argumento.
unregisterToken
opcional – um token de cancelamento de registro. Pode ser passado para cancelar o registro de um objeto antes que o coletor de lixo o exclua. Normalmente, o objeto target
é usado como unregisterToken
, que é a prática padrão.
unregister(unregisterToken)
– o método unregister
é usado para cancelar o registro de um objeto do registro. É necessário um argumento – unregisterToken
(o token de cancelamento de registro obtido ao registrar o objeto).
Agora vamos passar para um exemplo simples. Vamos usar o objeto user
já conhecido e criar uma instância de FinalizationRegistry
:
deixe usuário = {nome: "John" }; registro const = new FinalizationRegistry((heldValue) => { console.log(`${heldValue} foi coletado pelo coletor de lixo.`); });
Em seguida, registraremos o objeto, que requer um callback de limpeza chamando o método register
:
registro.register(usuário, nome do usuário);
O registro não mantém uma referência forte ao objeto que está sendo registrado, pois isso prejudicaria sua finalidade. Se o registro mantivesse uma referência forte, o objeto nunca seria coletado como lixo.
Se o objeto for excluído pelo coletor de lixo, nosso retorno de chamada de limpeza poderá ser chamado em algum momento no futuro, com o heldValue
passado para ele:
// Quando o objeto de usuário for excluído pelo coletor de lixo, a seguinte mensagem será impressa no console: "John foi recolhido pelo coletor de lixo."
Existem também situações em que, mesmo em implementações que utilizam um callback de limpeza, há uma chance de ele não ser chamado.
Por exemplo:
Quando o programa encerra totalmente sua operação (por exemplo, ao fechar uma aba em um navegador).
Quando a própria instância FinalizationRegistry
não estiver mais acessível ao código JavaScript. Se o objeto que cria a instância FinalizationRegistry
sair do escopo ou for excluído, os retornos de chamada de limpeza registrados nesse registro também poderão não ser invocados.
Voltando ao nosso exemplo de cache fraco , podemos notar o seguinte:
Embora os valores agrupados no WeakRef
tenham sido coletados pelo coletor de lixo, ainda existe um problema de “vazamento de memória” na forma das chaves restantes, cujos valores foram coletados pelo coletor de lixo.
Aqui está um exemplo de cache aprimorado usando FinalizationRegistry
:
function buscarImg() { //função abstrata para download de imagens... } functionfracoRefCache(fetchImg) { const imgCache = novo Mapa(); registro const = new FinalizationRegistry((imgName) => { // (1) const cachedImg = imgCache.get(imgNome); if (cachedImg && !cachedImg.deref()) imgCache.delete(imgName); }); return (imgNome) => { const cachedImg = imgCache.get(imgNome); if (cachedImg?.deref()) { retornar cachedImg?.deref(); } const newImg = fetchImg(imgNome); imgCache.set(imgName, new WeakRef(newImg)); registro.register(newImg, imgName); // (2) retornar novoImg; }; } const getCachedImg=fracoRefCache(fetchImg);
Para gerenciar a limpeza de entradas de cache “mortas”, quando os objetos WeakRef
associados são coletados pelo coletor de lixo, criamos um registro de limpeza FinalizationRegistry
.
O ponto importante aqui é que no callback de limpeza deve ser verificado se a entrada foi excluída pelo coletor de lixo e não adicionada novamente, para não excluir uma entrada “ativa”.
Depois que o novo valor (imagem) é baixado e colocado no cache, nós o registramos no registro do finalizador para rastrear o objeto WeakRef
.
Esta implementação contém apenas pares chave/valor reais ou “ativos”. Neste caso, cada objeto WeakRef
é registrado no FinalizationRegistry
. E depois que os objetos forem limpos pelo coletor de lixo, o retorno de chamada de limpeza excluirá todos os valores undefined
.
Aqui está uma representação visual do código atualizado:
Um aspecto importante da implementação atualizada é que os finalizadores permitem a criação de processos paralelos entre o programa “principal” e os retornos de chamada de limpeza. No contexto do JavaScript, o programa “principal” – é o nosso código JavaScript, que é executado e executado em nosso aplicativo ou página da web.
Portanto, desde o momento em que um objeto é marcado para exclusão pelo coletor de lixo até a execução real do retorno de chamada de limpeza, pode haver um certo intervalo de tempo. É importante entender que durante esse intervalo de tempo, o programa principal pode fazer qualquer alteração no objeto ou até mesmo trazê-lo de volta à memória.
É por isso que, no retorno de chamada de limpeza, devemos verificar se uma entrada foi adicionada de volta ao cache pelo programa principal para evitar a exclusão de entradas “ativas”. Da mesma forma, ao procurar uma chave no cache, há uma chance de que o valor tenha sido excluído pelo coletor de lixo, mas o retorno de chamada de limpeza ainda não tenha sido executado.
Tais situações requerem atenção especial se você estiver trabalhando com FinalizationRegistry
.
Passando da teoria à prática, imagine um cenário da vida real, onde um usuário sincroniza suas fotos em um dispositivo móvel com algum serviço de nuvem (como iCloud ou Google Fotos) e deseja visualizá-las em outros dispositivos. Além da funcionalidade básica de visualização de fotos, esses serviços oferecem muitos recursos adicionais, por exemplo:
Edição de fotos e efeitos de vídeo.
Criando “memórias” e álbuns.
Montagem de vídeo a partir de uma série de fotos.
…e muito mais.
Aqui, como exemplo, usaremos uma implementação bastante primitiva de tal serviço. O ponto principal – é mostrar um cenário possível de uso conjunto WeakRef
e FinalizationRegistry
na vida real.
Aqui está o que parece:
No lado esquerdo, há uma biblioteca de fotos na nuvem (elas são exibidas como miniaturas). Podemos selecionar as imagens que necessitamos e criar uma colagem, clicando no botão “Criar colagem” no lado direito da página. Em seguida, a colagem resultante pode ser baixada como uma imagem.
Para aumentar a velocidade de carregamento da página, seria razoável baixar e exibir miniaturas de fotos em qualidade compactada . Mas, para criar uma colagem a partir de fotos selecionadas, baixe-as e use-as em tamanho real .
Abaixo podemos ver que o tamanho intrínseco das miniaturas é de 240x240 pixels. O tamanho foi escolhido propositalmente para aumentar a velocidade de carregamento. Além disso, não precisamos de fotos em tamanho real no modo de visualização.
Suponhamos que precisamos criar uma colagem de 4 fotos: selecione-as e clique no botão "Criar colagem". Nesta fase, a função weakRefCache
já conhecida por nós verifica se a imagem necessária está no cache. Caso contrário, ele faz o download da nuvem e o coloca no cache para uso posterior. Isso acontece para cada imagem selecionada:
Prestando atenção na saída do console, você pode ver quais das fotos foram baixadas da nuvem – isso é indicado por FETCHED_IMAGE . Como esta é a primeira tentativa de criar uma colagem, isso significa que nesta fase o “cache fraco” ainda estava vazio, e todas as fotos foram baixadas da nuvem e colocadas nela.
Mas, junto com o processo de download das imagens, há também um processo de limpeza de memória pelo coletor de lixo. Isso significa que o objeto armazenado no cache, ao qual nos referimos, usando uma referência fraca, é excluído pelo coletor de lixo. E nosso finalizador é executado com sucesso, excluindo assim a chave pela qual a imagem foi armazenada no cache. CLEANED_IMAGE nos notifica sobre isso:
A seguir, percebemos que não gostamos da colagem resultante e decidimos alterar uma das imagens e criar uma nova. Para isso, basta desmarcar a imagem desnecessária, selecionar outra e clicar novamente no botão “Criar colagem”:
Mas desta vez nem todas as imagens foram baixadas da rede, e uma delas foi retirada de um cache fraco: a mensagem CACHED_IMAGE nos informa sobre isso. Isso significa que no momento da criação da colagem, o coletor de lixo ainda não havia excluído nossa imagem, e corajosamente a retiramos do cache, reduzindo assim o número de solicitações de rede e acelerando o tempo total do processo de criação da colagem:
Vamos “brincar” um pouco mais, substituindo novamente uma das imagens e criando uma nova colagem:
Desta vez o resultado é ainda mais impressionante. Das 4 imagens selecionadas, 3 delas foram retiradas do cache fraco e apenas uma precisou ser baixada da rede. A redução na carga da rede foi de cerca de 75%. Impressionante, não é?
Claro, é importante lembrar que tal comportamento não é garantido e depende da implementação e operação específica do coletor de lixo.
Com base nisso, surge imediatamente uma questão completamente lógica: por que não usamos um cache comum, onde podemos gerenciar nós mesmos suas entidades, em vez de depender do coletor de lixo? Isso mesmo, na grande maioria dos casos não há necessidade de utilizar WeakRef
e FinalizationRegistry
.
Aqui, simplesmente demonstramos uma implementação alternativa de funcionalidade semelhante, usando uma abordagem não trivial com recursos de linguagem interessantes. Ainda assim, não podemos confiar neste exemplo, se necessitamos de um resultado constante e previsível.
Você pode abrir este exemplo na sandbox.
WeakRef
– projetado para criar referências fracas a objetos, permitindo que eles sejam excluídos da memória pelo coletor de lixo caso não existam mais referências fortes a eles. Isso é benéfico para lidar com o uso excessivo de memória e otimizar a utilização dos recursos do sistema em aplicativos.
FinalizationRegistry
– é uma ferramenta para registrar callbacks, que são executados quando objetos que não são mais fortemente referenciados são destruídos. Isto permite liberar recursos associados ao objeto ou realizar outras operações necessárias antes de excluir o objeto da memória.