← Курс/KeepAlive: кэширование компонентов#228 из 257+25 XP

KeepAlive: кэширование компонентов

Проблема без KeepAlive

Когда компонент убирается из DOM (например, при переключении вкладок), Vue по умолчанию его **уничтожает** — вызывается onUnmounted, всё состояние теряется. При возврате компонент создаётся заново с нуля.

<!-- БЕЗ KeepAlive — форма сбрасывается при переключении вкладок -->
<component :is="currentTab" />

Основной синтаксис

<KeepAlive> оборачивает динамические компоненты и кэширует их экземпляры:

<!-- С KeepAlive — состояние сохраняется -->
<KeepAlive>
  <component :is="currentTab" />
</KeepAlive>

Компонент больше не уничтожается — он просто «прячется». При возврате восстанавливается с прежним состоянием.

include и exclude

Кэшировать можно не все компоненты, а только выбранные:

<!-- По имени компонента (string) -->
<KeepAlive include="TabA,TabB">
  <component :is="current" />
</KeepAlive>

<!-- Массив -->
<KeepAlive :include="['TabA', 'TabB']">
  <component :is="current" />
</KeepAlive>

<!-- RegExp -->
<KeepAlive :include="/^Tab/">
  <component :is="current" />
</KeepAlive>

<!-- exclude — кэшировать всё КРОМЕ -->
<KeepAlive exclude="HeavyEditor">
  <component :is="current" />
</KeepAlive>

Имя компонента определяется из опции name или имени файла при <script setup>.

max — ограничение кэша

<!-- Кэшировать максимум 5 компонентов (LRU-стратегия) -->
<KeepAlive :max="5">
  <component :is="current" />
</KeepAlive>

При превышении лимита самый давно используемый компонент вытесняется из кэша и уничтожается.

Хуки жизненного цикла: onActivated / onDeactivated

Кэшированные компоненты не получают обычные хуки mounted/unmounted при переключении. Вместо них используются:

import { onActivated, onDeactivated } from 'vue'

// Вызывается каждый раз, когда компонент становится видимым
onActivated(() => {
  console.log('Компонент показан')
  fetchLatestData()      // Обновляем данные при возврате
  startPolling()         // Возобновляем опрос сервера
})

// Вызывается каждый раз, когда компонент скрывается
onDeactivated(() => {
  console.log('Компонент скрыт')
  stopPolling()          // Останавливаем лишние запросы
  saveScrollPosition()   // Сохраняем позицию прокрутки
})

При первом монтировании: onMounted → onActivated.

При скрытии: onDeactivated (без onUnmounted).

При уничтожении из кэша: onDeactivated → onUnmounted.

Практические сценарии использования

**Система вкладок** — самый распространённый случай:

<KeepAlive :include="cachedTabs" :max="10">
  <component :is="activeTab" :key="activeTab.name" />
</KeepAlive>

**Многошаговый wizard** — сохраняет данные заполненных шагов:

<KeepAlive>
  <component :is="currentStep" />
</KeepAlive>

Маршруты в Vue Router:

<RouterView v-slot="{ Component }">
  <KeepAlive :include="cachedRoutes">
    <component :is="Component" />
  </KeepAlive>
</RouterView>

Примеры

LRU-кэш — именно такой алгоритм использует KeepAlive с опцией max для вытеснения компонентов

// KeepAlive с max использует алгоритм LRU (Least Recently Used).
// Реализуем его и посмотрим, как кэшируются "компоненты".

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity
    this.cache = new Map()  // Map сохраняет порядок вставки
  }

  // Получить значение (обновляет "недавность" использования)
  get(key) {
    if (!this.cache.has(key)) return undefined
    // Перемещаем в конец (самый "свежий")
    const value = this.cache.get(key)
    this.cache.delete(key)
    this.cache.set(key, value)
    return value
  }

  // Добавить значение
  set(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key)  // удаляем старую позицию
    } else if (this.cache.size >= this.capacity) {
      // Вытесняем наименее недавно используемый (первый в Map)
      const lruKey = this.cache.keys().next().value
      const lruVal = this.cache.get(lruKey)
      console.log(`[LRU] Вытеснен: "${lruKey}" (onUnmounted)`)
      if (lruVal.onUnmounted) lruVal.onUnmounted()
      this.cache.delete(lruKey)
    }
    this.cache.set(key, value)
  }

  has(key) { return this.cache.has(key) }

  keys() { return [...this.cache.keys()] }
}

// Симуляция KeepAlive с max
class KeepAlive {
  constructor({ max = Infinity, include = null, exclude = null } = {}) {
    this.lru = new LRUCache(max)
    this.include = include  // Set имён или null (все)
    this.exclude = exclude  // Set имён или null (никакие)
    this.current = null
  }

  shouldCache(name) {
    if (this.exclude && this.exclude.has(name)) return false
    if (this.include && !this.include.has(name)) return false
    return true
  }

  activate(name, factory) {
    // Деактивируем текущий
    if (this.current) {
      console.log(`[KeepAlive] onDeactivated: "${this.current.name}"`)
      if (this.current.onDeactivated) this.current.onDeactivated()
    }

    let instance
    if (this.shouldCache(name) && this.lru.has(name)) {
      // Восстанавливаем из кэша
      instance = this.lru.get(name)
      console.log(`[KeepAlive] Восстановлен из кэша: "${name}"`)
    } else {
      // Создаём новый
      instance = factory()
      instance.name = name
      console.log(`[KeepAlive] Создан новый: "${name}" (onMounted)`)
      if (instance.onMounted) instance.onMounted()
    }

    // Кэшируем если нужно
    if (this.shouldCache(name)) {
      this.lru.set(name, instance)
    }

    console.log(`[KeepAlive] onActivated: "${name}"`)
    if (instance.onActivated) instance.onActivated()

    this.current = instance
    console.log('Кэш:', this.lru.keys())
    return instance
  }
}

// --- "Компоненты" ---
function makeTab(name) {
  let visitCount = 0
  return {
    name,
    visitCount,
    onMounted()    { console.log(`  [${name}] onMounted`) },
    onActivated()  { visitCount++; console.log(`  [${name}] onActivated (посещений: ${visitCount})`) },
    onDeactivated(){ console.log(`  [${name}] onDeactivated`) },
    onUnmounted()  { console.log(`  [${name}] onUnmounted (кэш вытеснен!)`) },
  }
}

// max=3 — кэшируем не более 3 компонентов
const keepAlive = new KeepAlive({ max: 3 })

console.log('--- Первые открытия ---')
keepAlive.activate('TabA', () => makeTab('TabA'))
keepAlive.activate('TabB', () => makeTab('TabB'))
keepAlive.activate('TabA', () => makeTab('TabA'))  // из кэша
keepAlive.activate('TabC', () => makeTab('TabC'))

console.log('\n--- Превышение max=3 ---')
keepAlive.activate('TabD', () => makeTab('TabD'))  // вытесняет TabB (LRU)
keepAlive.activate('TabB', () => makeTab('TabB'))  // пересоздаётся (был вытеснен)