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 的映射,鍵的生成規則如下:
- 若組件在路由或
keyprop 中顯式聲明,則直接使用; - 否則使用
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:
- 路由 A 的組件失活 →
deactivated - 路由 B 掛載 → 正常生命週期
- 回退到 A → keep-alive 發現 key 命中
- 取出舊實例 →
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,超出後最早訪問的組件會被銷燬。