【得物技術】Keep-alive 原理及業務解決方案

背景:在 B 端系統中,爲了使用方便咱們會在頁面設計中加上標籤頁相似瀏覽器上方標籤頁的功能,爲了使用體驗更加接近瀏覽器標籤頁,咱們須要針對路由進行緩存。本文主要介紹 Vue 項目針對不一樣業務場景如何利用 keep-alive 來實現標籤頁動態緩存。html

關於 keep-alive

keep-alive 是一個抽象組件,不會和子組件創建父子關係,也不會做爲節點渲染到頁面上。vue

關於抽象組件 Vue 的文檔沒有提這個概念,它有一個屬性 abstract 爲 true,在抽象組件的生命週期過程當中,咱們能夠對包裹的子組件監聽的事件進行攔截,也能夠對子組件進行 Dom 操做,從而能夠對咱們須要的功能進行封裝,而不須要關心子組件的具體實現。除了kepp-alive還有<transition><transition-group>等。node

做用

  • 能在組件切換過程當中將狀態保留在內存中,防止重複渲染DOM。
  • 避免反覆渲染影響頁面性能,同時也能夠很大程度上減小接口請求,減少服務器壓力。
  • 可以進行路由緩存和組件緩存。

Activated

keep-alive 的模式下多了 activated 這個生命週期函數, keep-alive 的聲明週期執行:算法

  • 頁面第一次進入,鉤子的觸發順序

created-> mounted-> activated,退出時觸發 deactivated 當再次進入(前進或者後退)時,只觸發 activated。vuex

  • 事件掛載的方法等,只執行一次的放在 mounted 中;組件每次進去執行的方法放在 activated 中。

keep-alive解析

渲染

keep-alive 是由 render 函數決定渲染結果,在開頭會獲取插槽內的子元素,調用 getFirstComponentChild 獲取到第一個子元素的 VNode。後端

const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
複製代碼

接着判斷當前組件是否符合緩存條件,組件名與 include 不匹配或與 exclude 匹配都會直接退出並返回 VNode,不走緩存機制。api

// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
  return vnode
}
複製代碼

匹配條件經過會進入緩存機制的邏輯,若是命中緩存,從 cache 中獲取緩存的實例設置到當前的組件上,並調整 key 的位置將其放到最後(LRU 策略)。 若是沒命中緩存,將當前 VNode 緩存起來,並加入當前組件的 key。若是緩存組件的數量超出 max 的值,即緩存空間不足,則調用 pruneCacheEntry 將最舊的組件從緩存中刪除,即 keys[0] 的組件。以後將組件的 keepAlive 標記爲 true,表示它是被緩存的組件。數組

LRU 緩存策略:從內存中找出最久未使用的數據置換新的數據.算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是若是數據最近被訪問過,那麼未來被訪問的概率也更高。瀏覽器

const { cache, keys } = this
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
  ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
  vnode.componentInstance = cache[key].componentInstance
  // make current key freshest
  remove(keys, key)
  keys.push(key)
} else {
  cache[key] = vnode
  keys.push(key)
  // prune oldest entry
  if (this.max && keys.length > parseInt(this.max)) {
    pruneCacheEntry(cache, keys[0], keys, this._vnode)
  }
}
複製代碼

pruneCacheEntry 負責將組件從緩存中刪除,它會調用組件 $destroy 方法銷燬組件實例,緩存組件置空,並移除對應的 key。緩存

function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}
複製代碼

渲染總結

  • 經過 getFirstComponentChild 獲取第一個子組件,獲取該組件的 name;
  • 經過 include 與 exclude 屬性進行匹配,判斷當前組件是否要被緩存,若是匹配成功;
  • 命中緩存則直接獲取,同時更新 key 的位置;
  • 不命中緩存則設置進緩存,同時檢查緩存的實例數量是否超過 max, 超過則根據 LRU 策略刪除最近最久未使用;
  • 若是在中途有對 include 和 exclude 進行修改,經過 watch 來監聽 include 和 exclude,在其改變時調用 pruneCache 以修改 cache 緩存中的緩存數據。

基於 keep-alive 緩存實現方案

方案一:整個頁面緩存

通常採用在 router 的 meta 屬性裏增長一個 keepAlive 字段,而後在父組件或者根組件中,根據 keepAlive 字段的狀態使用 keep-alive 標籤,實現對路由的緩存:

<keep-alive>
    <router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" />
複製代碼

方案二:動態組件緩存

使用 vuex 配合 exclude 和 include,經過 include 和 exclude 決定那些組件進行緩存。注意這裏說的是組件,而且 cachedView 數組存放的是組件的名字,以下:

<keep-alive :include="$store.state.keepAlive.cachedView">
    <router-view></router-view>
</keep-alive>
複製代碼

場景分析

在 SPA 應用中用戶但願在 Tab 多個頁面來回切換的時候,不要丟失查詢的結果,關閉後清除緩存。

以下圖:

指望是用戶在切換 Tab 時 頁面時緩存的,當用戶關閉 Tab ,從新從左側菜單打開時是不緩存。

路由緩存方案

這樣是持久緩存了整個頁面,問題也就出現當用戶經過 Tab 關閉頁面而後從左側打開菜單時是緩存的頁面,這個不符合平常使用習慣,因此爲了解決數據新鮮度的問題能夠在 activated 觸發查詢請求就能保證數據的新鮮度。

activated(){
 getData()
}
複製代碼

可是使用後發現因爲你切換 Tab 時每次都會請求數據,可是若是項目的數據量有很大頻繁請求會給後端形成很大壓力 。

動態組件緩存方案

版本一須要頻繁拉去數據致使此方案已不合適只能動態緩存組件方案。

<keep-alive :include="cachedViews">
  <router-view :key="key"></router-view>
</keep-alive>
複製代碼

其中 cachedViews 是經過監聽路由動態增長刪除維護要緩存的組件名稱(因此組件名稱不要重複)數組:

const state = {
  cachedViews: [],
}
const mutations = {
  ADD_VIEWS: (state, view) => {
    if (state.cachedViews.includes(view.name)) return
    state.cachedViews.push(view.name)
  },
  DEL_CACHED_VIEW: (state, view) => {
    const index = state.cachedViews.indexOf(view.name)
    index > -1 && state.cachedViews.splice(index, 1)
  },
}
const actions = {
  addCachedView({ commit }, view) {
    commit('ADD_VIEWS', view)
  },
  deleteCachedView({ commit }, view) {
    commit('DEL_CACHED_VIEW', view)
  },
}
export default {
  namespaced: true,
  state,
  mutations,
  actions,
}
複製代碼

經過監聽路由變化:

watch: {
    '$route'(newRoute) {
      const { name } = newRoute
      const cacheRout = this.ISCACHE_MAP[name] || []
      cacheRout.map((item) => {
        store.dispatch('cached/addCachedView', { name: item })
      })
    },
  },
當經過 Tab 關閉頁面時清除組件名稱:
closeTag(newRoute) {
   const { name } = newRoute
   const cacheRout = this.ISCACHE_MAP[name] || []
   cacheRout.map((item) => {
     store.dispatch('cached/deleteCachedView', { name: item })
   })
 }
複製代碼

可是在遇到嵌套路由時在層級不一樣的 router-view 中切換 Tab 會出現緩存數據失效的問題,沒法緩存組件,嵌套路由以下:

如何解決?

  • 方案一:菜單嵌套,路由不嵌套

經過維護兩套數據,一套嵌套給左側菜單,一套扁平化後註冊路由,改造後的路由:

  • 方案二:修改 keep-alive 把 catch 對象到全局

經過上面 keep-alive 解析能夠知道,keep-alive就是把經過 include 匹配的組件的 vnode,放到 keep-alive 組件的一個 cache 對象中,下次渲染時,若是能在這裏面找到,就直接渲染vnode。因此把這個 cache 對象,放到全局去(全局變量或者 vuex),這樣我就能夠不用緩存 ParnetView 也能緩存其指定的子組件了。

import Vue from 'vue'
const cache = {}
const keys = []
export const removeCacheByName = (name) => {/* 省略移除代碼 */}
export default Object.assign({}, Vue.options.components.KeepAlive, {
  name: 'NewKeepAlive',
  created() {
    this.cache = cache
    this.keys = keys
  },
  destroyed() {},
})
複製代碼
  • 方案三:修改 keep-alive 根據路由 name 緩存

從上文能夠知道 keep-alive 是從 cache 中獲取緩存的實例設置到當前的組件上,key 是組件的名稱,能夠經過改造 getComponentName 方法,組件名稱獲取更改成路由名稱使其緩存的映射關係只與 route name 值有關係。

function getComponentName(opts) {
  return this.$route.name
}
複製代碼

cache 緩存 key 也更改成路由名稱。

參考連接

文|揣歪

關注得物技術,攜手走向技術的雲端

相關文章
相關標籤/搜索