記一次 緩存及其緩存優化原理

緩存淘汰策略

因爲 <keep-alive> 中的緩存優化遵循 LRU 原則,因此首先了解下緩存淘汰策略的相關介紹。

因爲緩存空間是有限的,因此不能無限制的進行數據存儲,當存儲容量達到一個閥值時,就會形成內存溢出,所以在進行數據緩存時,就要根據狀況對緩存進行優化,清除一些可能不會再用到的數據。因此根據緩存淘汰的機制不一樣,經常使用的有如下三種:html

  1. FIFO(fisrt-in-fisrt-out)- 先進先出策略

    咱們經過記錄數據使用的時間,當緩存大小即將溢出時,優先清楚離當前時間最遠的數據。vue

  1. LRU (least-recently-used)- 最近最少使用策略

    以時間做爲參考,若是數據最近被訪問過,那麼未來被訪問的概率會更高,若是以一個數組去記錄數據,當有一數據被訪問時,該數據會被移動到數組的末尾,代表最近被使用過,當緩存溢出時,會刪除數組的頭部數據,即將最不頻繁使用的數據移除。(keep-alive 的優化處理)node

  1. LFU (least-frequently-used)- 計數最少策略

    以次數做爲參考,用次數去標記數據使用頻率,次數最少的會在緩存溢出時被淘汰。react

<keep-alive> 簡單示例

首先咱們看一個動態組件使用 <keep-alive>例子)。api

<div id="dynamic-component-demo">
  <button v-on:click="currentTab = 'Posts'">Posts</button>
    <button v-on:click="currentTab = 'Archive'">Archive</button>
  <keep-alive>
    <component
      v-bind:is="currentTabComponent"
      class="tab"
    ></component>
  </keep-alive>
</div>
Vue.component('tab-posts', { 
  data: function () {
      return {
      count: 0
    }
  },
    template: `
      <div class="posts-tab">
     <button @click="count++">Click Me</button>
         <p>{{count}}</p>
    </div>`
})

Vue.component('tab-archive', { 
    template: '<div>Archive component</div>' 
})

new Vue({
  el: '#dynamic-component-demo',
  data: {
    currentTab: 'Posts',
  },
  computed: {
    currentTabComponent: function () {
      return 'tab-' + this.currentTab.toLowerCase()
    }
  }
})

咱們能夠看到,動態組件外層包裹着 <keep-alve> 標籤。數組

<keep-alive>
  <component
    v-bind:is="currentTabComponent"
    class="tab"
  ></component>
</keep-alive>

那就意味着,當選項卡 PostsArchive 在來回切換時,所對應的組件實例會被緩存起來,因此當再次切換到 Posts 選項時,其對應的組件 tab-posts 會從緩存中獲取,計數器 count 也會保留上一次的狀態。緩存

<keep-alive> 緩存及優化處理

就此,咱們看完 <keep-alive> 的簡單示例以後,讓咱們一塊兒來分析下源碼中它是如何進行組件緩存和緩存優化處理的。app

首次渲染

vue模板 -> AST -> render() -> vnode -> 真實Dom 這個轉化過程當中,會進入 patch 階段,在patch 階段,會調用 createElm 函數中會將 vnode 轉化爲真實 domdom

function createPatchFunction (backend) {
  ...
  //生成真實dom
  function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    // 返回 true 表明爲 vnode 爲組件 vnode,將中止接下來的轉換過程
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return;
    }
    ...
  }
}

在轉化節點的過程當中,由於 <keep-alive>vnode 會視爲組件 vnode,所以一開始會調用 createComponent() 函數,createComponent() 會執行組件初始化內部鉤子 init(), 對組件進行初始化和實例化。函數

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    var i = vnode.data;
    if (isDef(i)) {
      // isReactivated 用來判斷組件是否緩存
      var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        // 執行組件初始化的內部鉤子 init()
        i(vnode, false /* hydrating */);
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue);
        // 將真實 dom 添加到父節點,insert 操做 dom api
        insert(parentElm, vnode.elm, refElm);
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
        }
        return true
      }
    }
  }

<keep-alive> 組件經過調用內部鉤子 init() 方法進行初始化操做。

注:源碼中經過函數 installComponentHooks() 可追蹤到內部鉤子的定義對象 componentVNodeHooks
// inline hooks to be invoked on component VNodes during patch
var componentVNodeHooks = {
  init: function init (vnode, hydrating) {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      var mountedNode = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    } else {
      // 第一次運行時,vnode.componentInstance 不存在 ,vnode.data.keepAlive 不存在
      // 將組件實例化,並賦值給 vnode 的 componentInstance 屬性
      var child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      );
      // 進行掛載
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
  },
  // prepatch 是 patch 過程的核心步驟
  prepatch: function prepatch (oldVnode, vnode) { ... },
  insert: function insert (vnode) { ... },
  destroy: function destroy (vnode) { ... }
};

第一次執行時,很明顯組件 vnode 沒有 componentInstance 屬性,vnode.data.keepAlive 也沒有值,因此會調用 createComponentInstanceForVnode() 將組件進行實例化並將組件實例賦值給 vnodecomponentInstance 屬性,最後執行組件實例的 $mount 方法進行實例掛載。

createComponentInstanceForVnode()是組件實例化的過程,組件實例化無非就是一系列選項合併,初始化事件,生命週期等初始化操做。

緩存 vnode 節點

<keep-alive> 在執行組件實例化以後會進行組件的掛載(如上代碼所示)。

...
// 進行掛載
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
...

掛載 $mount 階段會調用 mountComponent() 函數進行 vm._update(vm._render(), hydrating); 操做。

Vue.prototype.$mount = function (el, hydrating) {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating)
};

function mountComponent (vm, el, hydrating) {
  vm.$el = el;
    ...
  callHook(vm, 'beforeMount');
  var updateComponent;
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    ...
  } else { 
    updateComponent = function () {
      // vm._render() 會根據數據的變化爲組件生成新的 Vnode 節點
      // vm._update() 最終會爲 Vnode 生成真實 DOM 節點
      vm._update(vm._render(), hydrating);
    }
  }
  ...
  return vm
}

vm._render() 函數最終會調用組件選項中的 render() 函數,進行渲染。

function renderMixin (Vue) {
  ...
  Vue.prototype._render = function () {
    var vm = this;
    var ref = vm.$options;
    var render = ref.render;
    ...
    try {  
      ...
      // 調用組件的 render 函數
      vnode = render.call(vm._renderProxy, vm.$createElement);
    }
    ...
    return vnode
  };
}

因爲keep-alive 是一個內置組件,所以也擁有本身的 render() 函數,因此讓咱們一塊兒來看下 render() 函數的具體實現。

var KeepAlive = {
  ...
  props: {
    include: patternTypes,  // 名稱匹配的組件會被緩存,對外暴露 include 屬性 api
    exclude: patternTypes,  // 名稱匹配的組件不會被緩存,對外暴露 exclude 屬性 api
    max: [String, Number]  // 能夠緩存的組件最大個數,對外暴露 max 屬性 api
  },
  created: function created () {},
  destroyed: function destroyed () {},
    mounted: function mounted () {},
  
  // 在渲染階段,進行緩存的存或者取
  render: function render () {
    // 首先拿到 keep-alve 下插槽的默認值 (包裹的組件)
    var slot = this.$slots.default;
    // 獲取第一個 vnode 節點
    var vnode = getFirstComponentChild(slot); // # 3802 line
    // 拿到第一個子組件實例
    var componentOptions = vnode && vnode.componentOptions;
    // 若是 keep-alive 第一個組件實例不存在
    if (componentOptions) {
      // check pattern
      var name = getComponentName(componentOptions);
      var ref = this;
      var include = ref.include;
      var exclude = ref.exclude;
      // 根據匹配規則返回 vnode 
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      var ref$1 = this;
      var cache = ref$1.cache;
      var keys = ref$1.keys;
      var key = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        // 獲取本地組件惟一key
        ? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
        : vnode.key;
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        // 使用 LRU 最近最少緩存策略,將命中的 key 從緩存數組中刪除,並將當前最新 key 存入緩存數組的末尾
        remove(keys, key); // 刪除命中已存在的組件
        keys.push(key); // 將當前組件名從新存入數組最末端
      } else {
        // 進行緩存
        cache[key] = vnode;
        keys.push(key);
        // prune oldest entry
        // 根據組件名與 max 進行比較
        if (this.max && keys.length > parseInt(this.max)) { // 超出組件緩存最大數的限制
          // 執行 pruneCacheEntry 對最少訪問數據(數組的第一項)進行刪除
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }
      // 爲緩存組件打上標誌
      vnode.data.keepAlive = true;
    }
    // 返回 vnode 
    return vnode || (slot && slot[0])
  }
};

從上可得知,在 keep-alive 的源碼定義中, render() 階段會緩存 vnode 和組件名稱 key 等操做。

  • 首先會判斷是否存在緩存,若是存在,則直接從緩存中獲取組件的實例,並進行緩存優化處理(稍後會介紹到)。
  • 若是不存在緩存,會將 vnode 做爲值存入 cache 對象對應的 key 中。還會將組件名稱存入 keys 數組中。
if (cache[key]) {
  vnode.componentInstance = cache[key].componentInstance;
  // make current key freshest
  // 使用 LRU 最近最少緩存策略,將命中的 key 從緩存數組中刪除,並將當前最新 key 存入緩存數組的末尾
  remove(keys, key); // 刪除命中已存在的組件
  keys.push(key); // 將當前組件名從新存入數組最末端
} else {
  // 進行緩存
  cache[key] = vnode;
  keys.push(key);
  // prune oldest entry
  // 根據組件名與 max 進行比較
  if (this.max && keys.length > parseInt(this.max)) { // 超出組件緩存最大數的限制
    // 執行 pruneCacheEntry 對最少訪問數據(數組的第一項)進行刪除
    pruneCacheEntry(cache, keys[0], keys, this._vnode);
  }
}

緩存真實 DOM

回顧以前提到的首次渲染階段,會調用 createComponent()函數, createComponent()會執行組件初始化內部鉤子 init(),對組件進行初始化和實例化等操做。

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  var i = vnode.data;
  if (isDef(i)) {
    // isReactivated 用來判斷組件是否緩存
    var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
    if (isDef(i = i.hook) && isDef(i = i.init)) {
        // 執行組件初始化的內部鉤子 init()
      i(vnode, false /* hydrating */);
    }
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue);
      // 將真實 dom 添加到父節點,insert 操做 dom api
      insert(parentElm, vnode.elm, refElm);
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
      }
      return true
    }
  }
}

createComponet() 函數還會咱們經過 vnode.componentInstance 拿到了 <keep-alive> 組件的實例,而後執行 initComponent()initComponent() 函數的做用就是將真實的 dom 保存再 vnode 中。

...
if (isDef(vnode.componentInstance)) {
  // 其中的一個做用就是保存真實 dom 到 vnode 中
  initComponent(vnode, insertedVnodeQueue);
  // 將真實 dom 添加到父節點,(insert 操做 dom api)
  insert(parentElm, vnode.elm, refElm);
  if (isTrue(isReactivated)) {
      reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
  }
  return true
}
...
function initComponent (vnode, insertedVnodeQueue) {
    if (isDef(vnode.data.pendingInsert)) {
      insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
      vnode.data.pendingInsert = null;
    }
    // 保存真是 dom 節點到 vnode 
    vnode.elm = vnode.componentInstance.$el;
    ...
}

緩存優化處理

在文章開頭,咱們介紹了三種緩存優化策略(它們各有優劣),而在 vue 中對 <keep-alive> 的緩存優化處理的實現上,便用到了上述的 LRU 緩存策略 。

上面介紹到,<keep-alive> 組件在存取緩存的過程當中,是在渲染階段進行的,因此咱們回過頭來看 render() 函數的實現。

var KeepAlive = {
  ...
  props: {
    include: patternTypes,  // 名稱匹配的組件會被緩存,對外暴露 include 屬性 api
    exclude: patternTypes,  // 名稱匹配的組件不會被緩存,對外暴露 exclude 屬性 api
    max: [String, Number]  // 能夠緩存的組件最大個數,對外暴露 max 屬性 api
  },
  // 建立節點生成緩存對象
  created: function created () {
    this.cache = Object.create(null); // 緩存 vnode 
    this.keys = []; // 緩存組件名
  },
 
  // 在渲染階段,進行緩存的存或者取
  render: function render () {
    // 首先拿到 keep-alve 下插槽的默認值 (包裹的組件)
    var slot = this.$slots.default;
    // 獲取第一個 vnode 節點
    var vnode = getFirstComponentChild(slot); // # 3802 line
    // 拿到第一個子組件實例
    var componentOptions = vnode && vnode.componentOptions;
    // 若是 keep-alive 第一個組件實例不存在
    if (componentOptions) {
      // check pattern
      var name = getComponentName(componentOptions);
      var ref = this;
      var include = ref.include;
      var exclude = ref.exclude;
      // 根據匹配規則返回 vnode 
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      var ref$1 = this;
      var cache = ref$1.cache;
      var keys = ref$1.keys;
      var key = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        // 獲取本地組件惟一key
        ? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
        : vnode.key;
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        // 使用 LRU 最近最少緩存策略,將命中的 key 從緩存數組中刪除,並將當前最新 key 存入緩存數組的末尾
        remove(keys, key); // 刪除命中已存在的組件
        keys.push(key); // 將當前組件名從新存入數組最末端
      } else {
        // 進行緩存
        cache[key] = vnode;
        keys.push(key);
        // prune oldest entry
        // 根據組件名與 max 進行比較
        if (this.max && keys.length > parseInt(this.max)) { // 超出組件緩存最大數的限制
          // 執行 pruneCacheEntry 對最少訪問數據(數組的第一項)進行刪除
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }
      // 爲緩存組件打上標誌
      vnode.data.keepAlive = true;
    }
    // 返回 vnode 
    return vnode || (slot && slot[0])
  }
};

<keep-alive> 組件會在建立階段生成緩存對象,在渲染階段對組件進行緩存,並進行緩存優化。咱們重點來看下段帶代碼。

if (cache[key]) {
  ...
  // 使用 LRU 最近最少緩存策略,將命中的 key 從緩存數組中刪除,並將當前最新 key 存入緩存數組的末尾
  remove(keys, key); // 刪除命中已存在的組件
  keys.push(key); // 將當前組件名從新存入數組最末端
} else {
  // 進行緩存
  cache[key] = vnode;
  keys.push(key);
  // 根據組件名與 max 進行比較
  if (this.max && keys.length > parseInt(this.max)) { // 超出組件緩存最大數的限制
    // 執行 pruneCacheEntry 對最少訪問數據(數組的第一項)進行刪除
    pruneCacheEntry(cache, keys[0], keys, this._vnode);
  }
}

從註釋中咱們能夠得知,當 keep-alive 被激活時(觸發 activated 鉤子),會執行 remove(keys, key) 函數,從緩存數組中 keys 刪除已存在的組件,以後會進行 push 操做,將當前組件名從新存入 keys 數組的最末端,正好符合 LRU

LRU:以時間做爲參考,若是數據最近被訪問過,那麼未來被訪問的概率會更高,若是以一個數組去記錄數據,當有一數據被訪問時,該數據會被移動到數組的末尾,代表最近被使用過,當緩存溢出時,會刪除數組的頭部數據,即將最不頻繁使用的數據移除。
remove(keys, key); // 刪除命中已存在的組件
keys.push(key); // 將當前組件名從新存入數組最末端

function remove (arr, item) {
  if (arr.length) {
    var index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

至此,咱們能夠回過頭看咱們上邊的 <keep-alive> 示例,示例中包含 tab-poststab-archive 兩個組件,經過 componentis 屬性動態渲染。當 tab 來回切換時,會將兩個組件的 vnode 和組件名稱存入緩存中,以下。

keys = ['tab-posts', 'tab-archive']
cache = {
    'tab-posts':   tabPostsVnode,
    'tab-archive': tabArchiveVnode
}

假如,當再次激活到 tabPosts 組件時,因爲命中了緩存,會調用源碼中的 remove()方法,從緩存數組中 keys tab-posts 刪除,以後會使用 push 方法將 tab-posts 推到末尾。這時緩存結果變爲:

keys = ['tab-archive', 'tab-posts']
cache = {
    'tab-posts':   tabPostsVnode,
    'tab-archive': tabArchiveVnode
}

如今咱們能夠得知,keys 用開緩存組件名是用來記錄緩存數據的。 那麼當緩存溢出時, <keep-alive>又是如何 處理的呢?

咱們能夠經過 max 屬性來限制最多能夠緩存多少組件實例。

在上面源碼中的 render() 階段,還有一個 pruneCacheEntry(cache, keys[0], keys, this._vnode) 函數,根據 LRU 淘汰策略,會在緩存溢出時,刪除緩存中的頭部數據,因此會將 keys[0] 傳入pruneCacheEntry()

if (this.max && keys.length > parseInt(this.max)) { // 超出組件緩存最大數的限制
  // 執行 pruneCacheEntry 對最少訪問數據(數組的第一項)進行刪除
  pruneCacheEntry(cache, keys[0], keys, this._vnode);
}

pruneCacheEntry() 具體邏輯以下:

  • 首先,經過cached$$1 = cache[key]` 獲取頭部數據對應的值 `vnode`,執行 `cached$$1.componentInstance.$destroy() 將組件實例銷燬。
  • 其次,執行 cache[key] = null 清空組件對應的緩存節點。
  • 最後,執行 remove(keys, key) 刪除緩存中的頭部數據 keys[0]

至此,關於 <keep-alive> 組件的首次渲染、組件緩存和緩存優化處理相關的實現就到這裏。

最後

最後記住這幾個點:

  • <keep-alive>vue 內置組件,在源碼定義中,也具備本身的組件選項如 datamethodcomputedprops 等。
  • <keep-alive> 具備抽象組件標識 abstract,一般會與動態組件一同使用。
  • <keep-alive> 包裹動態組件時,會緩存不活動的組件實例,將它們停用,而不是銷燬它們。
  • <keep-alive> 緩存的組件會觸發 activateddeactivated 生命週期鉤子。
  • <keep-alive> 會緩存組件實例的 vnode 對象 ,和真實 dom 節點,因此會有 max 屬性設置。
  • <keep-alive> 不會在函數式組件中正常工做,由於它們沒有緩存實例。
相關文章
相關標籤/搜索