Visão Geral do keep-alive
Ao desenvolver aplicações Vue, é comum alternar entre diferentes componentes dinâmicos. Em certas situações, queremos preservar o estado desses componentes e evitar que sejam recriados do zero cada vez que são exibidos. O componente intrínseco <keep-alive> do Vue.js oferece uma solução elegante para esse problema, permitindo que instâncias de componentes inativos sejam armazenadas em cache em vez de destruídas.
O objetivo principal de <keep-alive> é manter instâncias de componentes na memória, evitando a recriação e a reexecução de seus ciclos de vida (como created ou mounted) ao alternar entre eles. Isso é especialmente útil para otimizar o desempenho e garantir uma experiência de usuário mais fluida, onde o estado do componente (dados de formulário, posição de scroll, etc.) precisa ser mantido.
Utilização Básica
Para utilizar <keep-alive>, basta envolver o componente dinâmico com ele. Assim como <transition>, o <keep-alive> não renderiza um elemento DOM próprio e não aparece na árvore de componentes.
<keep-alive>
<component :is="componenteAtivo" class="secao"></component>
</keep-alive>
Quando um componente é envolvido por <keep-alive>, ele não é destruído e recriado novamente ao sair de vista. Em vez disso, ele é "desativado" e, ao retornar, é "ativado". Isso significa que os ganchos de ciclo de vida created e destroyed não serão disparados nas alternâncias. Em vez disso, dois novos ganchos são introduzidos para componentes dentro de <keep-alive>:
activated: Chamado quando o componente armazenado em cache é ativado.deactivated: Chamado quando o componente é desativado (sai de vista ou é removido do cache).
Um exemplo de como usar esses ganchos em um componente seria:
export default {
name: 'MeuComponenteCacheado',
data() {
return {
mensagem: 'Olá, estou ativo!'
};
},
activated() {
console.log('Componente MeuComponenteCacheado foi ativado.');
// Lógica para carregar dados ou restaurar estado
},
deactivated() {
console.log('Componente MeuComponenteCacheado foi desativado.');
// Lógica para salvar dados ou limpar recursos
},
mounted() {
console.log('Componente MeuComponenteCacheado foi montado.'); // Só ocorre na primeira vez
},
beforeDestroy() {
console.log('Componente MeuComponenteCacheado será destruído.'); // Só ocorre se for removido do cache
}
};
Controle de Cache com Propriedades
O <keep-alive> pode receber até três propriedades para controlar quais componentes devem ser armazenados em cache. A correspondência é feita com base na opção name do componente. Se name não estiver disponível, o nome de registro local (chave no objeto components do componente pai) é usado. Componentes anônimos não podem ser correspondidos.
include: (String | RegExp | Array<String>) - Apenas componentes com nomes correspondentes serão armazenados em cache.exclude: (String | RegExp | Array<String>) - Componentes com nomes correspondentes não serão armazenados em cache. Se um componente corresponder aincludeeexclude,excludetem prioridade.max: (Number | String) - Define o número máximo de instâncias de componentes a serem armazenadas em cache. Quando esse limite é atingido, a instância de componente menos recentemente acessada é destruída para dar lugar à nova.
Exemplos de uso dessas propriedades:
<!-- Cachear apenas 'ComponenteA' e 'ComponenteB' (string separada por vírgulas) -->
<keep-alive include="ComponenteA, ComponenteB">
<component :is="componenteAtual"></component>
</keep-alive>
<!-- Cachear 'ComponenteA', 'ComponenteB' ou 'ComponenteC' (com expressão regular) -->
<keep-alive :include="/^(ComponenteA|ComponenteB|ComponenteC)$/">
<component :is="componenteAtual"></component>
</keep-alive>
<!-- Cachear 'ComponenteA' e 'ComponenteB' (com array) -->
<keep-alive :include="['ComponenteA', 'ComponenteB']">
<component :is="componenteAtual"></component>
</keep-alive>
<!-- Excluir 'ComponenteDeLogin' do cache -->
<keep-alive exclude="ComponenteDeLogin">
<component :is="componenteAtual"></component>
</keep-alive>
<!-- Manter no máximo 5 componentes em cache -->
<keep-alive :max="5">
<component :is="componenteAtual"></component>
</keep-alive>
É importante notar que <keep-alive> destina-se a ser usado para um único filho direto que alterna. Não funcionará corretamente com v-for ou se houver múltiplos filhos condicionais que são renderizados simultaneamente. O <keep-alive> espera apenas um nó de componente filho para ser armazenado em cache por vez.
Como o keep-alive Funciona Internamente
A implementação do <keep-alive> no Vue.js envolve a gestão de um cache interno de instâncias de VNodes e um controle da ordem de acesso para a política LRU (Least Recent Used) quando o max é especificado.
Inicialização e Monitoramento
No ciclo de vida created, o <keep-alive> inicializa:
cache: Um objeto para armazenar os VNodes em cache, indexados por uma chave única.keys: Um array para manter a ordem das chaves dos componentes no cache, facilitando a implementação da política LRU.
Durante o mounted, ele configura observadores ($watch) para as propriedades include e exclude. Se essas propriedades mudarem, uma função de limpeza de cache é acionada para remover quaisquer componentes que não correspondam mais aos novos critérios de filtragem.
export default {
name: 'KeepAlive',
abstract: true, // Componente abstrato: não renderiza elemento DOM próprio
created() {
this.cache = Object.create(null); // Objeto para armazenar VNodes em cache
this.keys = []; // Array para manter a ordem das chaves para política LRU
},
mounted() {
// Monitora mudanças em 'include' para limpar o cache se necessário
this.$watch('include', (valorNovo) => {
this.limpaCache(this, (nome) => this.verificaCorrespondencia(valorNovo, nome));
});
// Monitora mudanças em 'exclude' para limpar o cache se necessário
this.$watch('exclude', (valorNovo) => {
this.limpaCache(this, (nome) => !this.verificaCorrespondencia(valorNovo, nome));
});
},
methods: {
// Função auxiliar para verificar se o nome de um componente corresponde a um padrão
verificaCorrespondencia(padrao: string | RegExp | string[], nomeComponente: string): boolean {
if (Array.isArray(padrao)) {
return padrao.includes(nomeComponente);
} else if (typeof padrao === 'string') {
return padrao.split(',').some(p => p.trim() === nomeComponente);
} else if (padrao instanceof RegExp) { // Assume isRegExp como utilitário ou verificação direta
return padrao.test(nomeComponente);
}
return false;
},
// Função para limpar o cache com base em um filtro
limpaCache(instanciaKeepAlive: any, filtro: Function) {
const { cache, keys, _vnode } = instanciaKeepAlive;
for (const chave in cache) {
const vnodeCacheado: any = cache[chave];
if (vnodeCacheado) {
const nome: string = this.getComponentName(vnodeCacheado.componentOptions);
if (nome && !filtro(nome)) {
this.removeEntradaCache(cache, chave, keys, _vnode);
}
}
}
},
// Função para remover uma entrada específica do cache
removeEntradaCache(
cache: any,
chave: string,
chaves: string[],
vnodeAtual?: any
) {
const cacheado = cache[chave];
if (cacheado && (!vnodeAtual || cacheado.tag !== vnodeAtual.tag)) {
// Se o VNode cacheado existir e não for o VNode atualmente renderizado, destruí-lo
cacheado.componentInstance.$destroy();
}
cache[chave] = null; // Marca a entrada como nula
const index = chaves.indexOf(chave);
if (index > -1) {
chaves.splice(index, 1); // Remove a chave do array de chaves
}
},
// Método auxiliar para obter o nome do componente (simplificado para o exemplo)
getComponentName(componentOptions: any): string | null {
return componentOptions ? (componentOptions.Ctor.options.name || componentOptions.tag) : null;
}
}
};
Lógica de Renderização
A função render do <keep-alive> é onde a magia acontece. Ela intercepta o VNode do componente filho e aplica a lógica de cache:
- **Obtenção do VNode Filho:** Primeiro, ele tenta obter o primeiro VNode de componetne filho direto do slot padrão.
- **Verificação de Filtro:** Se houver um componente filho, ele extrai seu nome e verifica se ele deve ser incluído ou excluído do cache com base nas propriedades
includeeexclude. Se o componente não passar nos filtros, ele é simplesmente retornado para ser renderizaod normalmente (sem cache). - **Gerenciamento de Cache:**
- **Geração de Chave:** Uma chave única é gerada para o componente (usando
vnode.keyou uma combinação dociddo construtor etag). - **Componente no Cache?** Se o componente já estiver no cache:
- A instância do componente existente é reutilizada (
vnode.componentInstance = cache[key].componentInstance). - A chave é movida para o final do array
keys, marcando-o como o "mais recentemente usado" (LRU).
- A instância do componente existente é reutilizada (
- **Componente Novo:** Se o componente não estiver no cache:
- Ele é adicionado ao
cachee sua chave é adicionada ao final do arraykeys. - **Limite
max:** Se a propriedademaxfor definida e o número de itens no cache exceder esse limite, o componente mais antigo (primeiro emkeys) é removido do cache e destruído.
- Ele é adicionado ao
- **Geração de Chave:** Uma chave única é gerada para o componente (usando
- **Marcação do VNode:** Finalmente, o VNode do componente é marcado com
vnode.data.keepAlive = true, indicando que ele deve ser tratado como um componente cacheado pelo renderizador do Vue. - **Retorno:** O VNode é retornado para ser renderizado.
export default {
// ... created, mounted, methods ...
render() {
const slotPadrao = this.$slots.default;
const vnodeFilho: any = this.getFirstComponentChild(slotPadrao); // Obtém o primeiro VNode filho
const opcoesComponente: any = vnodeFilho && vnodeFilho.componentOptions;
if (opcoesComponente) {
const nomeComponente: string = this.getComponentName(opcoesComponente);
const { include, exclude } = this;
// Verifica se o componente deve ser incluído ou excluído do cache
if (
(include && (!nomeComponente || !this.verificaCorrespondencia(include, nomeComponente))) ||
(exclude && nomeComponente && this.verificaCorrespondencia(exclude, nomeComponente))
) {
return vnodeFilho; // Não armazenar em cache, renderizar normalmente
}
const { cache, keys } = this;
const chaveUnica: string = vnodeFilho.key == null
? opcoesComponente.Ctor.cid + (opcoesComponente.tag ? `::${opcoesComponente.tag}` : '')
: vnodeFilho.key;
if (cache[chaveUnica]) {
// Componente já em cache, reutilizar instância
vnodeFilho.componentInstance = cache[chaveUnica].componentInstance;
// Mover a chave para o final para marcar como 'mais recentemente usado' (LRU)
this.remove(keys, chaveUnica);
keys.push(chaveUnica);
} else {
// Novo componente, adicionar ao cache
cache[chaveUnica] = vnodeFilho;
keys.push(chaveUnica);
// Se 'max' definido e cache excedeu o limite, remover o mais antigo
if (this.max && keys.length > parseInt(this.max as string)) {
this.removeEntradaCache(cache, keys[0], keys, this._vnode);
}
}
vnodeFilho.data.keepAlive = true; // Marca o VNode como cacheado
}
return vnodeFilho || (slotPadrao && slotPadrao[0]);
},
methods: {
// ... getComponentName, verificaCorrespondencia, limpaCache, removeEntradaCache ...
// Função auxiliar para obter o primeiro VNode de componente filho
getFirstComponentChild(children: any[]): any {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child && child.componentOptions) {
return child;
}
}
}
return null;
},
// Função auxiliar para remover item de um array
remove(arr: any[], item: any) {
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1);
}
}
}
}
};