Vue keep-alive 使用及緩存機制詳解

前言在VUE項目中,有些組件或者頁面沒必要多次渲染,所以需要將部分組件有條件的在內存中進行"持久化",不過這裏的持久化不是簡單的數據持久化,而是整個組件(包括數據和視圖)的持久化,剛好VUE提供了<keep-alive>這個內置組件來完成這件事情。<keep-alive> 包裹動態組件時,會緩存不活動的組件實例,而不是銷燬它們。和 <transition> 相似,<keep-alive> 是一個抽象組件:它自身不會渲染一個 DOM 元素,也不會出現在組件的父組件鏈中。當組件在 <keep-alive> 內被切換,它的 activated和 deactivated 這兩個生命週期鉤子函數將會被對應執行。
基本使用使用的時候分兩個版本。在vue 2.1.0 之前,大部分是這樣實現的:

[JavaScript] 純文本查看 複製代碼

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

<keep-alive>

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

</keep-alive>

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

new Router({

    routes: [

        {

            name: 'a',

            path: '/a',

            component: A,

            meta: {

                keepAlive: true

            }

        },

        {

            name: 'b',

            path: '/b',

            component: B

        }

    ]

})


這樣配置路由的路由元信息之後,a路由的$route.meta.keepAlive便爲 true ,而b路由則爲 false 。所以爲 true 的將被包裹在 keep-alive 中,爲 false 的則在外層。這樣a路由便達到了被緩存的效果,如果還有想要緩存的路由,只需要在路由元中加入 keepAlive: true 即可。
在vue 2.1.0 版本之後,keep-alive 新加入了兩個屬性: include(包含的組件緩存生效) 與 exclude(排除的組件不緩存,優先級大於include) 。include 和 exclude 屬性允許組件有條件地緩存。二者都可以用逗號分隔字符串、正則表達式或一個數組來表示。當使用正則或者是數組時,一定要使用 v-bind 。
簡單使用:

[JavaScript] 純文本查看 複製代碼

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

<!-- 逗號分隔字符串 -->

<keep-alive include="a,b">

  <component :is="view"></component>

</keep-alive>

<!-- 正則表達式 (使用 `v-bind`) -->

<keep-alive :include="/a|b/">

  <component :is="view"></component>

</keep-alive>

<!-- 數組 (使用 `v-bind`) -->

<keep-alive :include="['a', 'b']">

  <component :is="view"></component>

</keep-alive>

配合router-view使用

<template>

  <div id="app">

    <transition :name="routerTransition">

      <keep-alive :include="keepAliveComponentsData">

        <router-view :key="$route.fullPath"></router-view>

      </keep-alive>

    </transition>

  </div>

</template>


推薦使用2.1.0以後的版本來做緩存策略,代碼更加簡潔,而且少了很多的重複渲染。
高級進階使用我們瞭解了基本的使用,但是在日常項目中可沒有想象的那麼簡單,所以需要設計一下整個項目的緩存策略,如何讓所有組件可以動態的去切換自己的緩存屬性是一個值得考慮的問題。
業務場景1.列表頁進入詳情頁,詳情頁中有頭也有行列表。
2.從詳情頁的行列表進入行詳情頁查看後返回詳情頁詳情頁不刷新,如果行詳情頁修改後,我進入詳情頁需要刷新。
這樣的業務場景在移動端非常常見,當然解決方法也有很多,比如每次返回詳情頁傳遞給詳情頁一個標誌之類的,然後在詳情頁做判斷,根據標誌去獲取store裏面的數據還是接口最新數據,這樣做判斷不僅麻煩,而且無法做到頁面級的緩存,而且耗費內存資源。如果使用好了keep-alive,你可以輕鬆實現如上效果。
整體設計思路1.在store裏面寫三個方法:setKeepAlive,setNoKeepAlive,getKeepAlive,作用分別是設置需要緩存的組件,清除不需要緩存的組件,獲取緩存的組件。
2.獲得所有的組件的name屬性,並將其存入store裏面的一個數組中。
3.在App.vue掛載的時候去獲取緩存的組件數組。
4.在頁面路由攔截函數中去動態設置頁面是不是要緩存。
5.在App.vue中監聽store的變化,對include對應的數組進行賦值。
具體實現1.store裏面註冊三個方法

[JavaScript] 純文本查看 複製代碼

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

const state = {

  keepAliveComponents:[],

}

const getters = {

  getKeepAlive (state) {

    return state.keepAliveComponents

  },

}

const mutations = {

  setKeepAlive (state, component) {

    // 判斷keepAliveComponents中是否存在之前設置過的組件名,避免重複

    !state.keepAliveComponents.includes(component) && state.keepAliveComponents.push(component)

  },

  setNoKeepAlive (state, component) {

    // 刪除不要緩存的組件

    const index = state.keepAliveComponents.indexOf(component)

    index !== -1 && state.keepAliveComponents.splice(index, 1)

  },

}

const actions = {}

export default {

  state,

  getters,

  mutations,

  actions,

}


2.路由集合中去設置每個組件都緩存。

[JavaScript] 純文本查看 複製代碼

?

1

2

3

4

5

6

routes.forEach((item) => {

    // 在路由全局鉤子beforeEach中,根據keepAlive屬性,統一設置頁面的緩存性

  // 作用是每次進入該組件,就將它緩存

  store.commit('setKeepAlive', item.name)

 })

export default routes


⚠️注意:這裏沒有寫成store.commit('setKeepAlive', item.component.name),而是item.name。本應該寫成store.commit('setKeepAlive', item.component.name),因爲include接受的是組件的名,但是在按需加載的情況下打包後這個name會變化,所以在開始設計你的項目的時候儘量保證路由名和組件名一致。
3.在App.vue掛載的時候去獲取緩存的組件數組,默認全部緩存。

[JavaScript] 純文本查看 複製代碼

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

<template>

  <div id="app">

    <transition :name="routerTransition">

      <keep-alive :include="keepAliveComponentsData">

        <router-view :key="$route.fullPath"></router-view>

      </keep-alive>

    </transition>

  </div>

</template>

<script>

import Vue from 'vue'

import store from './store'

export default {

  name: 'App',

  data () {

    return {

      keepAliveComponentsData: [],

    }

  },

  mounted () {

    // 掛載獲取需要緩存的組件

    this.keepAliveComponentsData = store.getters.getKeepAlive

  }

}

</script>


4.動態改變頁面的緩存屬性。
比如我要從詳情頁跳轉行詳情了,跳轉之前我不能讓行詳情有緩存,如果行詳情有緩存的話,每次進去都是一樣的。所以我要清除緩存。
">
我現在需要從行詳情跳轉到其他關聯的單據了,那我肯定需要緩存一下行詳情了,不然回到行詳情啥都沒有了。
">
5.監聽緩存變化。
動態設置可以了,我現在還需要去動態綁定到include的數組上,所以我需要在每次頁面跳轉的時候去監聽一下。

[JavaScript] 純文本查看 複製代碼

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

<template>

  <div id="app">

    <transition :name="routerTransition">

      <keep-alive :include="keepAliveComponentsData">

        <router-view :key="$route.fullPath"></router-view>

      </keep-alive>

    </transition>

  </div>

</template>

<script>

import Vue from 'vue'

import store from './store'

export default {

  name: 'App',

  data () {

    return {

      keepAliveComponentsData: [],

    }

  },

  mounted () {

    this.keepAliveComponentsData = store.getters.getKeepAlive

  },

  watch: {

    // 監聽路由變化動態設置include綁定的數據

    $route (to, from) {

      this.keepAliveComponentsData = store.getters.getKeepAlive

    },

  },

}

</script>


現在用法說完了,但是對其原理好像還不是很清楚,那就來看看他的實現方式。
原理解析keep-alive核心思想就是將組件緩存爲vnode,然後用include裏面的數組去匹配,匹配到就拿來直接用,如果exclude變化的話就銷燬對應的vnode。
源碼解析直接貼源碼。大概理解寫註釋裏面

[JavaScript] 純文本查看 複製代碼

?

001

002

003

004

005

006

007

008

009

010

011

012

013

014

015

016

017

018

019

020

021

022

023

024

025

026

027

028

029

030

031

032

033

034

035

036

037

038

039

040

041

042

043

044

045

046

047

048

049

050

051

052

053

054

055

056

057

058

059

060

061

062

063

064

065

066

067

068

069

070

071

072

073

074

075

076

077

078

079

080

081

082

083

084

085

086

087

088

089

090

091

092

093

094

095

096

097

098

099

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

import { isRegExp, remove } from 'shared/util'

import { getFirstComponentChild } from 'core/vdom/helpers/index'

type VNodeCache = { [key: string]: ?VNode };

// 獲取組件名

function getComponentName (opts: ?VNodeComponentOptions): ?string {

  return opts && (opts.Ctor.options.name || opts.tag)

}

// 一個檢測name是否匹配的函數

function matches (pattern: string | RegExp | Array<string>, name: string): boolean {

  // 數組

  if (Array.isArray(pattern)) {

    return pattern.indexOf(name) > -1

  } else if (typeof pattern === 'string') { //字符串

    return pattern.split(',').indexOf(name) > -1

  } else if (isRegExp(pattern)) { //正則

    return pattern.test(name)

  }

  /* istanbul ignore next */

  return false

}

// 修正cache

function pruneCache (keepAliveInstance: any, filter: Function) {

  const { cache, keys, _vnode } = keepAliveInstance

  for (const key in cache) {

    // 取出cache中的vnode

    const cachedNode: ?VNode = cache[key]

    if (cachedNode) {

      const name: ?string = getComponentName(cachedNode.componentOptions)

      /* name不符合filter條件的,同時不是目前渲染的vnode時,銷燬vnode對應的組件實例(Vue實例),並從cache中移除 */

      if (name && !filter(name)) {

        pruneCacheEntry(cache, key, keys, _vnode)

      }

    }

  }

}

function pruneCacheEntry (

  cache: VNodeCache,

  key: string,

  keys: Array<string>,

  current?: VNode

) {

  const cached = cache[key]

  if (cached && (!current || cached.tag !== current.tag)) {

    /* 銷燬vnode對應的組件實例(Vue實例) */

    cached.componentInstance.$destroy()

  }

  cache[key] = null

  remove(keys, key)

}

const patternTypes: Array<Function> = [String, RegExp, Array]

export default {

  name: 'keep-alive',

  abstract: true,

  props: {

    include: patternTypes,

    exclude: patternTypes,

    max: [String, Number]

  },

  created () {

    /* 緩存對象 */

    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進行修正 */

    this.$watch('include', val => {

      pruneCache(this, name => matches(val, name))

    })

    this.$watch('exclude', val => {

      pruneCache(this, name => !matches(val, name))

    })

  },

  render () {

    /* 得到slot插槽中的第一個組件 */

    const slot = this.$slots.default

    const vnode: VNode = getFirstComponentChild(slot)

    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions

    if (componentOptions) {

      // check pattern

      /* 獲取組件名稱,優先獲取組件的name字段,否則是組件的tag */

      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

      }

      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,還未緩存過則進行緩存 */

      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)

        }

      }

            /* keepAlive標記位 */

      vnode.data.keepAlive = true

    }

    return vnode || (slot && slot[0])

  }

}


簡單總結1.created鉤子會創建一個cache對象,用來作爲緩存容器,保存vnode節點。destroyed鉤子則在組件被銷燬的時候清除cache緩存中的所有組件實例。
2.在render函數中主要做了這些事情:
• 首先通過getFirstComponentChild獲取第一個子組件,獲取該組件的name(存在組件名則直接使用組件名,否則會使用tag)。
• 接下來會將這個name通過include與exclude屬性進行匹配,匹配不成功(說明不需要進行緩存)則不進行任何操作直接返回vnode,vnode是一個VNode類型的對象。
• include與exclude屬性支持字符串如"a,b,c"這樣組件名以逗號隔開的情況以及正則表達式。matches通過這兩種方式分別檢測是否匹配當前組件。
• 然後根據key在this.cache中查找,如果存在則說明之前已經緩存過了,直接將緩存的vnode的componentInstance(組件實例)覆蓋到目前的vnode上面,否則將vnode存儲在cache中。最後返回vnode(有緩存時該vnode的componentInstance已經被替換成緩存中的了)。
3.需要監聽改變就用watch來監聽pruneCache與pruneCache這兩個屬性的改變,在改變的時候修改cache緩存中的緩存數據。
4.Vue.js內部將DOM節點抽象成了一個個的VNode節點,keep-alive組件的緩存也是基於VNode節點的而不是直接存儲DOM結構。它將滿足條件(pruneCache與pruneCache)的組件在cache對象中緩存起來,在需要重新渲染的時候再將vnode節點從cache對象中取出並渲染。


文章轉載自:https://juejin.im/post/5eb143676fb9a0437d2b2275
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章