Vue 2深入 keep-alive

keep-alive 是 Vue 2 運行時裏最常被提及卻最少被深究的內置組件。它看上去只是“把頁面緩存起來”,背後卻涉及實例生命週期劫持、LRU 緩存策略、VNode 複用以及內存管理。

一、設計動機

單頁應用裏常見的“標籤頁”“麪包屑”“分步表單”等交互模式,都要求用户在多個路由或狀態之間來回切換。默認情況下,每一次切換都會觸發舊組件的 $destroy,再執行新組件的完整掛載鏈路:

beforeDestroy → destroy → 回收 DOM → 創建 DOM → beforeCreate → created → mounted

如果頁面重、接口多,這種“拆房再建房”的代價極高,而且無法保留滾動位置、表單草稿等瞬態狀態。

keep-alive 通過緩存組件實例(而非僅緩存 DOM)解決了兩個問題:

  • 時間成本:跳過創建與銷燬,直接複用已有實例;
  • 狀態保留:實例存活,內部 state、DOM 引用、定時器全部保持原狀。

二、組件級緩存的三要素

keep-alive 在 created 鈎子裏初始化兩個核心字段:

this.cache = Object.create(null)  // 組件緩存池
this.keys = []                   // 緩存鍵的有序索引

緩存池是一個鍵到 VNode 的映射,鍵的生成規則如下:

  • 若組件在路由或 key prop 中顯式聲明,則直接使用;
  • 否則使用 cid + "::" + tag 自動生成,確保全局唯一。

keys 數組用來維護 LRU 順序:最近一次被命中的鍵總是被移動到數組末尾,最久未使用的鍵位於頭部,當緩存數量超過 max 時直接 shift 掉。

三、渲染函數:命中、失活、淘汰的決策點

keep-alive 沒有模板,它的邏輯全部寫在 render

render () {
  const vnode = getFirstComponentChild(this.$slots.default)
  const key   = vnode.key == null
                ? generateKey(vnode)
                : vnode.key

  const { cache, keys } = this

  if (cache[key]) {                       // —— 命中緩存
    vnode.componentInstance = cache[key].componentInstance
    remove(keys, key)
    keys.push(key)                        // 維護 LRU
  } else {                                // —— 首次出現
    cache[key] = vnode
    keys.push(key)
    if (this.max && keys.length > this.max) {
      pruneCacheEntry(cache, keys[0])     // 淘汰最久未使用
    }
  }

  vnode.data.keepAlive = true             // 打上標記,阻止後續銷燬
  return vnode
}

關鍵點拆解:

  • 複用實例:直接複用 componentInstance,包括內部狀態、DOM 引用、事件監聽。
  • 阻止銷燬:子組件在 destroy 階段會檢查 vnode.data.keepAlive,為真則跳過 $destroy,僅執行 deactivated
  • LRU 淘汰:pruneCacheEntry 會手動調用 $destroy 並移除 DOM,確保內存不會無限膨脹。

四、生命週期劫持:activated 與 deactivated

被 keep-alive 包裹的組件新增兩條專用鈎子:

  • activated:組件從緩存池取出並插入 DOM 後觸發;首次掛載在 mounted 之後立即執行一次。
  • deactivated:組件從 DOM 移除但實例存活時觸發,此時 DOM 已卸載,定時器、事件監聽仍可運行。

常見用法:

activated () {
  this.$refs.scroll && this.$refs.scroll.restore()
},
deactivated () {
  this.timer && clearInterval(this.timer)
}

五、緩存命中時的完整調用鏈

用户從路由 A 切換到路由 B,再回退到 A:

  1. 路由 A 的組件失活 → deactivated
  2. 路由 B 掛載 → 正常生命週期
  3. 回退到 A → keep-alive 發現 key 命中
  4. 取出舊實例 → activated → DOM 重新插入 → 頁面瞬間恢復

整個過程無 beforeCreate / created / mounted,也無 DOM 重建,僅有 CSS 動畫或滾動恢復邏輯。

六、內存與邊界注意事項

  • max 必須設置:未設置時緩存無限增長,切頁面多會撐爆內存。
  • 避免緩存龐大狀態:緩存的是實例 + DOM 樹,包含所有閉包變量;大數據列表或第三方圖表應手動 deactivated 中銷燬。
  • keep-alive 不能緩存異步組件本身,只能緩存異步組件解析後的真實組件實例;若需緩存加載態,把 <Suspense> 與 keep-alive 組合使用。
  • include / exclude 支持正則與函數,可基於路由 meta 動態調整緩存策略,實現“登錄頁不緩存,業務頁緩存”。

七、代碼實例

<template>
  <div>
    <button @click="id = id === 'a' ? 'b' : 'a'">toggle</button>
    <keep-alive :max="10" include="A,B">
      <comp-a v-if="id === 'a'" />
      <comp-b v-else />
    </keep-alive>
  </div>
</template>

切換路由或條件渲染時,<comp-a><comp-b> 的實例被緩存;max 限制為 10,超出後最早訪問的組件會被銷燬。