天天看點

Vue <keep-alive> 首次渲染、元件緩存和緩存優化處理 源碼實作

Vue keep-alive 源碼實作

    • keep-alive使用
      • 結合router,緩存部分頁面
    • keep-alive源碼如何實作元件緩存和緩存優化處理
      • 緩存淘汰政策
        • FIFO(fisrt-in-fisrt-out)- 先進先出政策
        • LRU (least-recently-used)- 最近最少使用政策
        • LFU (least-frequently-used)- 計數最少政策
      • \ 簡單示例
      • 緩存及優化處理(源碼)
        • 首次渲染
        • 緩存 vnode 節點
        • 緩存真實 DOM
        • 緩存優化處理
        • 最後記住這幾個點:

keep-alive使用

keep-alive官方文檔

在動态元件上使用 keep-alive

prop:

  • include

    - 字元串或正規表達式。隻有名稱比對的元件會被緩存。
  • exclude

    - 字元串或正規表達式。任何名稱比對的元件都不會被緩存。
  • max

    - 數字。最多可以緩存多少元件執行個體。

<keep-alive>

是Vue的内置元件,能在

元件切換過程

中将狀态保留在

記憶體

中,防止

重複渲染DOM

<keep-alive> 包裹動态元件時,會緩存不活動的元件執行個體,而不是銷毀它們。和 <transition> 相似,<keep-alive> 是一個

抽象元件

:它自身不會渲染一個 DOM 元素,也不會出現在父元件鍊中。

當元件在 <keep-alive> 内被切換,它的 activated 和 deactivated 這兩個生命周期鈎子函數将會被對應執行。

結合router,緩存部分頁面

使用

$route.meta

keepAlive

屬性:

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

需要在

router

中設定

router的元資訊meta

//...router.js
export default new Router({
  routes: [
  ...
    {
      path: '/page1',
      name: 'Page1',
      component: Page1,
      meta: {
        keepAlive: true // 緩存
      }
    },
    {
      path: '/page2',
      name: 'Page2',
      component: Page2,
      meta: {
        keepAlive: false // 不緩存
      }
    }
  ]
})
           

以上面router的代碼為例:

<!-- Page1元件 -->
<template>
  <div class="page1">
    <h1>Vue</h1>
    <h2>{{msg}}</h2>
    <input placeholder="輸入框"></input>
  </div>
</template>
           
<!-- Page2元件 -->
<template>
  <div class="page2">
    <h1>{{msg}}</h1>
    <input placeholder="輸入框"></input>
  </div>
</template>
           

不同元件切換過程,有緩存的元件不會被銷毀,也不用重新渲染

上面例子,可以看到,每次切換到page1元件時,可以看到input框依舊是上次填寫的内容,不會重置,而page2元件的input框會重置

也可以通過

路由的beforeRouteLeave(to, from, next)鈎子

動态設定

route.meta

keepAlive

屬性來實作其他需求

keep-alive源碼如何實作元件緩存和緩存優化處理

緩存淘汰政策

<keep-alive>

中的緩存優化遵循

LRU 原則

,是以了解下緩存淘汰政策。

由于緩存空間是有限的,是以不能無限制的進行資料存儲,當存儲容量達到一個閥值時,就會造成

記憶體溢出

,是以在進行資料緩存時,就要根據情況對緩存進行優化,清除一些可能不會再用到的資料。

是以根據緩存淘汰的機制不同,常用的有以下三種:

FIFO(fisrt-in-fisrt-out)- 先進先出政策

通過記錄資料使用的時間,當緩存大小即将溢出時,優先

清除離目前時間最遠的資料

Vue <keep-alive> 首次渲染、元件緩存和緩存優化處理 源碼實作

LRU (least-recently-used)- 最近最少使用政策

以時間作為參考,如果資料最近被通路過,那麼将來被通路的幾率會更高,如果以一個數組去記錄資料,當有一資料被通路時,該資料會被移動到數組的末尾,表明最近被使用過,當緩存溢出時,會删除數組的頭部資料,即

将最不頻繁使用的資料移除

。(keep-alive 的優化處理)

Vue <keep-alive> 首次渲染、元件緩存和緩存優化處理 源碼實作

LFU (least-frequently-used)- 計數最少政策

以次數作為參考,用次數去标記資料使用頻率,

次數最少的會在緩存溢出時被淘汰

Vue <keep-alive> 首次渲染、元件緩存和緩存優化處理 源碼實作

<keep-alive> 簡單示例

首先我們看一個動态元件使用 的例子

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

那就意味着,當選項卡 Posts 、 Archive 在來回切換時,所對應的元件執行個體會被緩存起來,是以當再次切換到 Posts 選項時,其對應的元件 tab-posts 會從緩存中擷取,計數器 count 也會保留上一次的狀态。

緩存及優化處理(源碼)

就此,我們看完

<keep-alive>

的簡單示例之後,讓我們一起來分析下源碼中它是如何進行元件緩存和緩存優化處理的。

首次渲染

vue 在模闆 -> AST -> render() -> vnode -> 真實Dom 這個轉化過程中,會進入

patch

階段,在patch 階段,會調用

createElm 函數

中會

将 vnode 轉化為真實 dom

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

将元件進行執行個體化并将元件執行個體指派給

vnode

componentInstance

屬性,最後執行元件執行個體的

$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 中對 的緩存優化處理的實作上,便用到了上述的

LRU

緩存政策 。

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 。

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

至此,我們可以回過頭看我們上邊的 示例,示例中包含 tab-posts、tab-archive 兩個元件,通過 component 的 is 屬性動态渲染。當 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 内置元件,在源碼定義中,也具有自己的元件選項如 data 、 method 、 computed 、 props 等。
  • <keep-alive> 具有抽象元件辨別

    abstract

    ,通常會與動态元件一同使用。
  • <keep-alive> 包裹動态元件時,會緩存不活動的元件執行個體,将它們停用,而不是銷毀它們。
  • 被 <keep-alive> 緩存的元件會觸發 activated 或 deactivated 生命周期鈎子。
  • <keep-alive> 會緩存元件執行個體的 vnode 對象 ,和真實 dom 節點,是以會有 max 屬性設定。
  • <keep-alive> 不會在函數式元件中正常工作,因為它們沒有緩存執行個體。

keep-alive實作原理

Vue源碼解析,keep-alive是如何實作緩存的?

謝謝你閱讀到了最後

期待你關注、收藏、評論、點贊