Entrada front-end (vue) para curso de proficiência: Entrar para aprender JavaScript não fornece nenhuma operação de gerenciamento de memória. Em vez disso, a memória é gerenciada pela VM JavaScript por meio de um processo de recuperação de memória chamado coleta de lixo .
Como não podemos forçar a coleta de lixo, como sabemos que está funcionando? Quanto sabemos sobre isso?
A execução do script é pausada durante este processo
Libera memória para recursos inacessíveis
é incerto
Não verifica toda a memória de uma só vez, mas é executado em vários ciclos
É imprevisível, mas funcionará quando necessário
Isso significa que não há necessidade de se preocupar com problemas de alocação de recursos e memória? Claro que não. Se não tomarmos cuidado, podem ocorrer alguns vazamentos de memória.
Um vazamento de memória é um bloco de memória alocada que o software não consegue recuperar.
Javascript fornece um coletor de lixo, mas isso não significa que podemos evitar vazamentos de memória. Para ser elegível para coleta de lixo, o objeto não deve ser referenciado em outro lugar. Se você mantiver referências a recursos não utilizados, isso impedirá que esses recursos sejam recuperados. Isso é chamado de retenção de memória inconsciente .
Vazamento de memória pode fazer com que o coletor de lixo seja executado com mais frequência. Como esse processo impedirá a execução do script, poderá fazer com que nosso programa congele. Se tal atraso ocorrer, os usuários exigentes certamente perceberão que, se não ficarem satisfeitos com isso, o produto ficará offline por um longo tempo. Mais seriamente, isso pode causar o travamento de todo o aplicativo, o que é gg.
Como evitar vazamentos de memória O principal é evitar a retenção de recursos desnecessários. Vejamos alguns cenários comuns.
setInterval()
chama repetidamente uma função ou executa um fragmento de código, com um atraso fixo entre cada chamada. Ele retorna um ID
de intervalo ID
identifica exclusivamente o intervalo para que você possa excluí-lo posteriormente chamando clearInterval()
.
Criamos um componente que chama uma função de retorno de chamada para indicar que foi concluído após x
número de loops. Estou usando React neste exemplo, mas funciona com qualquer estrutura FE.
importar React, {useRef} de 'react'; const Timer = ({ ciclos, onFinish }) => { const currentCicles = useRef(0); setInterval(() => { if (currentCicles.current >= ciclos) { onFinish(); retornar; } currentCicles.current++; }, 500); retornar ( <p>Carregando...</p> ); } exportar temporizador padrão;
À primeira vista, parece não haver problema. Não se preocupe, vamos criar outro componente que acione esse timer e analisar o desempenho de sua memória.
importar React, {useState} de 'react'; importar estilos de '../styles/Home.module.css' importar temporizador de '../components/Timer'; função padrão de exportação Home() { const [showTimer, setShowTimer] = useState(); const onFinish = () => setShowTimer(false); retornar ( <p className={styles.container}> {showTimer? ( <Ciclos do temporizador={10} onFinish={onFinish} /> ): ( <botão onClick={() => setShowTimer(true)}> Tentar novamente </button> )} </p> ) }
Depois de alguns cliques no botão Retry
, aqui está o resultado do uso do Chrome Dev Tools para obter o uso de memória:
Quando clicamos no botão de nova tentativa, podemos ver que cada vez mais memória é alocada. Isto significa que a memória alocada anteriormente não foi liberada. O cronômetro ainda está funcionando em vez de ser substituído.
Como resolver este problema? O valor de retorno de setInterval
é um ID de intervalo, que podemos usar para cancelar esse intervalo. Neste caso específico, podemos chamar clearInterval
após o componente ser descarregado.
useEffect(() => { const intervaloId = setInterval(() => { if (currentCicles.current >= ciclos) { onFinish(); retornar; } currentCicles.current++; }, 500); return() => clearInterval(intervalId); }, [])
Às vezes é difícil encontrar esse problema ao escrever código. A melhor maneira é abstrair os componentes.
Usando React aqui, podemos agrupar toda essa lógica em um Hook personalizado.
importar {useEffect} de 'react'; exportar const useTimeout = (refreshCycle = 100, retorno de chamada) => { useEffect(() => { if (refreshCycle <= 0) { setTimeout(retorno de chamada, 0); retornar; } const intervaloId = setInterval(() => { ligar de volta(); }, atualizarCycle); return() => clearInterval(intervalId); }, [refreshCycle, setInterval, clearInterval]); }; exportar useTimeout padrão;
Agora sempre que precisar usar setInterval
, você pode fazer isso:
const handleTimeout = () => ...; useTimeout(100, handleTimeout);
Agora você pode usar este useTimeout Hook
sem se preocupar com vazamentos de memória, o que também é um benefício da abstração.
A API Web fornece um grande número de ouvintes de eventos. Anteriormente, discutimos setTimeout
. Agora vamos dar uma olhada em addEventListener
.
Neste exemplo, criamos uma função de atalho de teclado. Como temos funções diferentes em páginas diferentes, serão criadas diferentes funções de teclas de atalho
function homeAtalhos({tecla}) { if (chave === 'E') { console.log('editar widget') } } // Quando o usuário faz login na página inicial, executamos document.addEventListener('keyup', homeShortcuts); // O usuário faz algo e então navega até a função de configurações settingsShortcuts({ key}) { if (chave === 'E') { console.log('editar configuração') } } // Quando o usuário faz login na página inicial, executamos document.addEventListener('keyup', settingsShortcuts);
Ainda parece bom, exceto que o keyup
anterior não é limpo ao executar o segundo addEventListener
. Em vez de substituir nosso ouvinte keyup
, este código adicionará outro callback
. Isso significa que quando uma tecla é pressionada, ela aciona duas funções.
Para limpar o retorno de chamada anterior, precisamos usar removeEventListener
:
document.removeEventListener('keyup', homeShortcuts);
Refatore o código acima:
function homeAtalhos({tecla}) { if (chave === 'E') { console.log('editar widget') } } // o usuário chega em casa e nós executamos document.addEventListener('keyup', homeShortcuts); //o usuário faz algumas coisas e navega até as configurações configurações de funçãoAtalhos({tecla}) { if (chave === 'E') { console.log('editar configuração') } } // o usuário chega em casa e nós executamos document.removeEventListener('keyup', homeShortcuts); document.addEventListener('keyup', configuraçõesShortcuts);
Como regra geral, tenha muito cuidado ao usar ferramentas de objetos globais.
Observadores são um recurso da API da Web do navegador que muitos desenvolvedores desconhecem. Isso é poderoso se você deseja verificar alterações na visibilidade ou no tamanho dos elementos HTML.
A interface IntersectionObserver
(parte da API Intersection Observer) fornece um método para observar de forma assíncrona o status da interseção de um elemento de destino com seus elementos ancestrais ou viewport
de documento de nível superior. O elemento ancestral e viewport
são chamados root
.
Embora seja poderoso, devemos usá-lo com cautela. Depois de terminar de observar um objeto, lembre-se de cancelá-lo quando não estiver em uso.
Dê uma olhada no código:
const referência = ... const visível = (visível) => { console.log(`É ${visível}`); } useEffect(() => { se (!ref){ retornar; } observador.atual = novo IntersectionObserver( (entradas) => { if (!entries[0].isIntersecting) { visível(verdadeiro); } outro { visível(falso); } }, { rootMargin: `-${header.height}px` }, ); observador.atual.observe(ref); }, [ref]);
O código acima parece bom. Porém, o que acontece com o observador quando o componente é descarregado. Ele não é limpo e a memória vaza? Como resolvemos esse problema? Basta usar o método disconnect
:
const referência = ... const visível = (visível) => { console.log(`É ${visível}`); } useEffect(() => { se (!ref){ retornar; } observador.atual = novo IntersectionObserver( (entradas) => { if (!entries[0].isIntersecting) { visível(verdadeiro); } outro { visível(falso); } }, { rootMargin: `-${header.height}px` }, ); observador.atual.observe(ref); return() => observador.atual?.disconnect(); }, [ref]);
Adicionar objetos a uma janela é um erro comum. Em alguns cenários, pode ser difícil encontrá-lo, especialmente ao usar this
em um contexto de execução de janela. Dê uma olhada no seguinte exemplo:
function addElement(elemento) { if (!this.stack) { esta.stack = { elementos: [] } } this.stack.elements.push(elemento); }
Parece inofensivo, mas depende de qual contexto você chama addElement
. Se você chamar addElement do contexto da janela, o heap aumentará.
Outro problema pode ser a definição incorreta de uma variável global:
var a = 'exemplo 1' // O escopo é limitado ao local onde var é criado b = 'exemplo 2' // Adicionado ao objeto Window;
Para evitar esse problema, você pode usar o modo estrito:
"usar estrito"
Ao usar o modo estrito, você sinaliza ao compilador JavaScript que deseja se proteger desses comportamentos. Você ainda pode usar o Windows quando precisar. No entanto, você deve usá-lo de forma explícita.
Como o modo estrito afeta nosso exemplo anterior:
Para a função addElement
, this
é indefinido quando chamado do escopo global
Se você não especificar const | let | var
em uma variável, receberá o seguinte erro:
ReferenceError não capturado: b não está definido
Os nós DOM também não estão imunes a vazamentos de memória. Precisamos ter cuidado para não salvar referências a eles. Caso contrário, o coletor de lixo não conseguirá limpá-los porque ainda estarão acessíveis.
Demonstre isso com um pequeno trecho de código:
elementos const = []; lista const = document.getElementById('lista'); função addElement() { //limpa nós lista.innerHTML = ''; const pElement= document.createElement('p'); elemento const = document.createTextNode(`adicionando elemento ${elements.length}`); pElement.appendChild(elemento); list.appendChild(pElement); elementos.push(pElement); } document.getElementById('addElement').onclick = addElement;
Observe que a função addElement
limpa a lista p
e adiciona um novo elemento a ela como elemento filho. Este elemento recém-criado é adicionado ao array elements
.
Na próxima vez que addElement
for executado, o elemento será removido da lista p
, mas não é adequado para coleta de lixo porque está armazenado na matriz elements
.
Monitoramos a função após executá-la algumas vezes:
Veja como o nó foi comprometido na imagem acima. Então, como resolver esse problema? Limpar a matriz elements
os tornará elegíveis para coleta de lixo.
Neste artigo, examinamos as formas mais comuns de vazamento de memória. É óbvio que o próprio JavaScript não vaza memória. Em vez disso, é causado pela retenção de memória não intencional por parte do desenvolvedor. Contanto que o código esteja limpo e não esqueçamos de limpar depois de nós mesmos, vazamentos não acontecerão.
Compreender como a memória e a coleta de lixo funcionam em JavaScript é fundamental. Alguns desenvolvedores têm a falsa sensação de que, por ser automático, não precisam se preocupar com esse problema.