keep-alive的實現原理及LRU緩存策略

文章首發於我的博客html

keep-alive 用法

咱們先來看看 官方文檔中keep-alive 的用法。前端

<keep-alive>
  <component :is="view"></component>
</keep-alive>
複製代碼
  • props:vue

    • include:只有名稱匹配的組件纔會被緩存
    • exclude: 任何名稱匹配的組件都不會被緩存
    • max: 最多能夠緩存多少組件實例。(2.5.0 新增, 一旦這個數字達到了,在新實例被建立以前,已緩存組件中最久沒有被訪問的實例會被銷燬掉)
  • 用法node

    • keep-alive 包裹動態組件時,會緩存不活動的組件實例,而不是銷燬他們。
    • 當組件在 keep-alive 內被切換, 它的 activated 和 deactivated 兩個生命週期鉤子函數將會被執行。

實現原理

源碼解析

<keep-alive>是vue源碼中實現的一個組件, 咱們能夠從源碼入手進行分析,基於vue 2.6.11 版本, 源碼位置src/core/components/keep-alive.jsgit

// <keep-alive> 組件的實現也是一個對象
export default {
  name: 'keep-alive',
  // 抽象組件
  abstract: true,

  props: {
    // 只有名稱匹配的組件纔會被緩存
    include: patternTypes,
    // 任何名稱匹配的組件都不會被緩存
    exclude: patternTypes,
    // 緩存組件的最大數量, 由於咱們緩存的是vnode對象,它也會持有DOM,當咱們緩存不少的時候,會比較佔用內存,因此該配置容許咱們指定緩存大小
    max: [String, Number]
  },

  created () {
    // 初始化存儲緩存的cache對象和緩存 vNode 鍵的數組
    this.cache = Object.create(null)
    this.keys = []
  },

  // destroyed 中銷燬全部cache中的組件實例
  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    // 監聽 include 和 exclude的變化,在變化的時候從新調整 cache的內容
    // 其實就是對 cache 作遍歷,發現緩存的節點名稱和新的規則沒有匹配上的時候,就把這個緩存節點從緩存中摘除
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },
  // 自定義render函數
  render () {
    /* * 獲取第一個子元素的 vnode * 因爲咱們也是在 <keep-alive> 標籤內部寫 DOM,因此能夠先獲取到它的默認插槽,而後再獲取到它的第一個子節點。<keep-alive> 只處理第一個子元素,因此通常和它搭配使用 * 的有 component 動態組件或者是 router-view,這點要牢記。 */
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)

    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      // 判斷當前組件名稱和 include、exclude 的關係:
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this

      // matches就是作匹配,分別處理了數組、字符串、正則表達式的狀況
      // 組件名若是知足了配置 include 且不匹配或者是配置了 exclude 且匹配,那麼就直接返回這個組件的 vnode,不然的話走下一步緩存:
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      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
      // 若是命中緩存,則直接從緩存中拿 vnode 的組件實例,而且從新調整了 key 的順序放在了最後一個
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        // 使用 LRU 緩存策略,把key移除,同時加在最後面
        remove(keys, key)
        keys.push(key)
      } else {
        // 沒有命中緩存,則把 vnode設置進緩存
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        // 配置了max 而且緩存的長度超過了 this.max,則要從緩存中刪除第一個
        if (this.max && keys.length > parseInt(this.max)) {
          // 除了從緩存中刪除外,還要判斷若是要刪除的緩存並的組件 tag 不是當前渲染組件 tag,也執行刪除緩存的組件實例的 $destroy 方法。
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }
      // keepAlive標記位
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}


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)
}

複製代碼

1. 判斷當前組件是否要被緩存

獲取 keep-alive 包裹的第一個子組件對象及其組件名,根據設置的 include/exclude(若是有)進行條件匹配,決定是否緩存。若是不匹配,則直接返回組件實例github

2. 命中緩存則直接獲取,同時更新key的位置

根據組件id和tag生成緩存 key,並在緩存對象中查找是否已緩存過該組件實例對象,若是存在,直接取出緩存值並更新該key在this.keys中的位置(更新key的位置是實現LRU置換策略的關鍵)正則表達式

3. 不命中緩存則設置進緩存,同時檢查緩存的實例數量是否超過 max

在this.cache對象中存儲該組件實例並保存 key 值,以後檢查緩存的實例數量是否超過 max的設置值,超過 max 的設置值,超過則根據 LRU 置換策略刪除最近最久未使用的實例(便是下標爲0的那個key)算法

4. 將當前組件實例的 keepAlive 屬性設置爲true,這個在緩存選中過程當中會用到。

abstract(抽象組件)

最開始設置的 abstract 屬性 值爲 true,是一個抽象組件,文檔中提到過: 是一個抽象組件:它自身不會渲染一個 DOM 元素,也不會出如今父組件鏈中。segmentfault

組件一旦被 緩存,那麼再次渲染的時候就不會執行 created、mounted 等鉤子函數。可是有些業務場景須要在被緩存的組件從新渲染的時候須要作一些事情,vue則提供了activated 和 deactivated 鉤子函數。api

vue在初始化生命週期的時候,爲組件實例創建父子關係時會根據 abstract 屬性決定是否忽略某個組件。在keep-alive中,設置了abstract:true,那Vue就會跳過該組件實例。

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
  ...
}
複製代碼



<keep-alive> 首次渲染和緩存渲染

首次渲染的時候,除了再 <keep-alive> 中創建緩存,設置vnode.data.keepAlive爲true,其餘的過程和普通組件同樣。

緩存渲染的時候,會根據 vnode.componentInstance(首次渲染vnode.componentInstance 爲 undefined) 和 vnode.data.keepAlive進行判斷不會執行組件的 created、mounted 等鉤子函數,而是對緩存的組件執行patch 過程,最後直接把緩存的DOM對象直接插入到目標元素中,完成了數據更新的狀況下的渲染過程。

緩存策略

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

最多見的實現是使用一個鏈表保存緩存數據,詳細算法實現以下:

  1. 新數據插入到鏈表頭部
  2. 每當緩存命中(即緩存數據被訪問),則將數據移到鏈表頭部
  3. 鏈表滿的時候,將鏈表尾部的數據丟棄。

總結

  • <keep-alive> 是一個抽象組件,
    • 首次渲染的時候設置緩存
    • 緩存渲染的時候不會執行組件的 created、mounted 等鉤子函數, 而是對緩存的組件執行patch 過程,最後直接更新到目標元素。
  • 使用 LRU 緩存策略對組件進行緩存
    • 命中緩存,則直接返回緩存,同時更新緩存key的位置
    • 不命中緩存則設置進緩存,同時檢查緩存的實例數量是否超過 max

參考

其餘

最近發起了一個100天前端進階計劃,主要是深挖每一個知識點背後的原理,歡迎關注 微信公衆號「牧碼的星星」,咱們一塊兒學習,打卡100天。

相關文章
相關標籤/搜索