用keep-alive优化页面性能

keep-alive

keep-alive是Vue提供的一个抽象组件,用来对组件进行缓存,从而节省性能,由于是一个抽象组件,所以在v页面渲染完毕后不会被渲染成一个DOM元素

1
2
3
4
5
6
7
8
//keep-alive 用来缓存组件,避免多次加载相应的组件,减少性能消耗
<template>
<div id="app">
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
</template>

  • <keep-alive>包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 相似, 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。

  • keep-alive内被切换时组件的activated、deactivated这两个生命周期钩子函数会被执行

  • 被包裹在keep-alive中的组件的状态将会被保留,例如我们将某个列表类组件内容滑动到第10条位置,那么我们在切换到一个组件后再次切换回到该组件,该组件的位置状态依旧会保持在第10条列表处.

我们在创建一个router实例的时候,可以提供一个scrollBehavior方法,该方法会在用户切换路由时触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const router=new VueRouter({
routes:[
{
path:"/",
component:Home
}
],
scrollBehavior(to,form,savedPosition){
//scrollBehavior方法接收to,form路由对象
//第三个参数savedPosition当且仅当在浏览器前进后退按钮触发时才可用
//该方法会返回滚动位置的对象信息,如果返回false,或者是一个空的对象,那么不会发生滚动
//我们可以在该方法中设置返回值来指定页面的滚动位置,例如:
return {x:0,y:0}
//表示在用户切换路由时让是所有页面都返回到顶部位置
//如果返回savedPosition,那么在点击后退按钮时就会表现的像原生浏览器一样,返回的页面会滚动过到之前按钮点击跳转的位置,大概写法如下:
if(savedPosition){
return savedPosition
}else{
return {x:0,y:0}
}
//如果想要模拟滚动到锚点的行为:
if(to.hash){
return {
selector:to.hash
}
}
}
})

还有一个方法就是利用我们上面说过的,在keep-alive激活会触发activated钩子函数,那么在该函数内设置scrollTop为0
被keep-alive包裹的动态组件或router-view会缓存不活动的实例,再次被调用这些被缓存的实例会被再次复用,对于我们的某些不是需要实时更新的页面来说大大减少了性能上的消耗,不需要再次发送HTTP请求,但是同样也存在一个问题就是被keep-alive包裹的组件我们请求获取的数据不会再重新渲染页面,这也就出现了例如我们使用动态路由做匹配的话页面只会保持第一次请求数据的渲染结果,所以需要我们在特定的情况下强制刷新某些组件

利用include、exclude属性

prop:

  • include: 字符串或正则表达式。只有匹配的组件会被缓存,keep-alive组件激活时调用。
  • exclude: 字符串或正则表达式。任何匹配的组件都不会被缓存,组件停用时调用。
1
2
3
4
5
6
<keep-alive include="bookLists,bookLists">
<router-view></router-view>
</keep-alive>
<keep-alive exclude="indexLists">
<router-view></router-view>
</keep-alive>

include属性表示只有name属性为bookLists,bookLists的组件会被缓存,(注意是组件的名字,不是路由的名字)其它组件不会被缓存exclude属性表示除了name属性为indexLists的组件不会被缓存,其它组件都会被缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//利用meta属性
export default[
{
path:'/',
name:'home',
components:Home,
meta:{
keepAlive:true //需要被缓存的组件
},
{
path:'/book',
name:'book',
components:Book,
meta:{
keepAlive:false //不需要被缓存的组件
}
]

<keep-alive>
<router-view v-if="this.$route.meat.keepAlive"></router-view>
<!--这里是会被缓存的组件-->
</keep-alive>
<keep-alive v-if="!this.$router.meta.keepAlive"></keep-alive>
<!--这里是不会被缓存的组件-->

当引入keep-alive的时候,页面第一次进入,钩子的触发顺序 created-> mounted-> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发activated。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//$route发生变化时再次赋值listId
watch: {
'$route'() {
this.listId = this.$route.params.id;
}
},
//通过activated钩子触发请求函数
activated() {
this.getDetail();
},
//返回详情页面时 隐藏内容div区块 再进入详情时 显示内容div区块
deactivated() {
this.isShowContent = false;
}

DEMO前进刷新,后退不刷新

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<div id="app">
<div class="title">F12,打开控制台,通过console查看数据变化</div>
<keep-alive>
<router-view v-if="$route.meta.keepAlive">
<!-- 这里是会被缓存的视图组件,比如 page1,page2 -->
</router-view>
</keep-alive>

<router-view v-if="!$route.meta.keepAlive">
<!-- 这里是不被缓存的视图组件,比如 page3 -->
</router-view>
</div>
</template>

<script>
export default {
name: "app"
};
</script>

<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.title {
font-size: 20px;
color: red;
margin-bottom: 40px;
}
</style>

router/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import Vue from 'vue'
import Router from 'vue-router'
import index from '@/components/index'
import page1 from '@/components/page1'
import page2 from '@/components/page2'
import page3 from '@/components/page3'

Vue.use(Router)

export default new Router({
routes: [{
path: '/',
name: 'index',
component: index,
meta: {
keepAlive: false, //此组件不需要被缓存
}
},
{
path: '/page1',
name: 'page1',
component: page1,
meta: {
keepAlive: true, //此组件需要被缓存
isBack: false, //用于判断上一个页面是哪个
}
},
{
path: '/page2',
name: 'page2',
component: page2,
meta: {
keepAlive: true, // 此组件需要被缓存
isBack: false, //用于判断上一个页面是哪个
}
},
{
path: '/page3',
name: 'page3',
component: page3,
meta: {
keepAlive: false, // 此组件不需要被缓存
}
}
],
mode: 'history',
scrollBehavior(to, from, savedPosition) {
if(savedPosition) {
return savedPosition
} else {
return {
x: 0,
y: 0
}
}
}
})

page.vue page1.vue page2.vue page3.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
//page.vue
<template>
<div>
<router-link to="page1">第一个页面</router-link>
<router-link to="page2">第二个页面</router-link>
<router-link to="page3">第三个页面</router-link>
</div>
</template>
<style scoped>
a{
display: block;
margin-top: 20px;
}
</style>



//page1.vue
<template>
<div class="page1">
<button @click="goBack">返回</button>
<p>{{msg}}</p>
<p>{{str}}</p>
<input type="text" v-model="val" />
<router-link to="page2">跳转到下一个页面</router-link>
</div>
</template>

<script>
export default {
name: "page1",
data() {
return {
val:"我是输入框",
msg: "我是第一个页面",
str: "", // 加载页面后执行获取数据的方法,插入到此
isFirstEnter: false // 是否第一次进入,默认false
};
},
methods: {
getData() {
// getData方法,模拟从后台请求数据
console.warn("我调用第一个页面getData方法了");
this.str = "我是通过调用方法加载的数据。。。";
},
goBack() {
this.$router.go(-1);
}
},
beforeRouteEnter(to, from, next) {
console.log("我是第一个页面的beforeRouteEnter方法");
// 路由导航钩子,此时还不能获取组件实例 `this`,所以无法在data中定义变量(利用vm除外)
// 参考 https://router.vuejs.org/zh-cn/advanced/navigation-guards.html
// 所以,利用路由元信息中的meta字段设置变量,方便在各个位置获取。这就是为什么在meta中定义isBack
// 参考 https://router.vuejs.org/zh-cn/advanced/meta.html
if(from.name == 'page2') {
to.meta.isBack = true;
//判断是从哪个路由过来的,如果是page2过来的,表明当前页面不需要刷新获取新数据,直接用之前缓存的数据即可
}

next();
},
created() {
console.log("我是第一个页面的 created 方法");
this.isFirstEnter = true;
// 只有第一次进入或者刷新页面后才会执行此钩子函数
// 使用keep-alive后(2+次)进入不会再执行此钩子函数
},
mounted() {
console.log("我是第一个页面的 mounted 方法");
},
activated() {
console.log("我是第一个页面的 activated 方法");
if(!this.$route.meta.isBack || this.isFirstEnter) {
// 如果isBack是false,表明需要获取新数据,否则就不再请求,直接使用缓存的数据
// 如果isFirstEnter是true,表明是第一次进入此页面或用户刷新了页面,需获取新数据
this.str = '' // 把数据清空,可以稍微避免让用户看到之前缓存的数据
this.getData();
}
// 恢复成默认的false,避免isBack一直是true,导致下次无法获取数据
this.$route.meta.isBack = false
// 恢复成默认的false,避免isBack一直是true,导致每次都获取新数据
this.isFirstEnter = false;

},
deactivated() {
console.log("我是第一个页面的 deactivated 方法");
},
destroyed() {
console.log("我是第一个页面的 destroyed 方法");
}
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>

</style>



//page2.vue
<template>
<div class="page2">
<button @click="goBack">返回</button>
<p>{{msg}}</p>
<p>{{str}}</p>
<input type="text" v-model="val" />
<router-link to="page3">跳转到下一个页面</router-link>
</div>
</template>

<script>
export default {
name: "page2",
data() {
return {
val:"我是输入框",
msg: "我是第二个页面",
str: "",
isFirstEnter: false
};
},
methods: {
getData() {
console.warn("我调用第二个页面getData方法了");
this.str = "我是通过调用方法加载的数据。。。";
},
goBack() {
this.$router.go(-1);
}
},
beforeRouteEnter(to, from, next) {
console.log('我是第二个页面的beforeRouteEnter方法')
if(from.name == 'page3') {
to.meta.isBack = true;
}
next()
},
created() {
console.log("我是第二个页面的 created 方法");
this.isFirstEnter = true;
},
mounted() {
console.log("我是第二个页面的 mounted 方法");
},
activated() {
console.log("我是第二个页面的 activated 方法");
if(!this.$route.meta.isBack || this.isFirstEnter) {
this.str = ''
this.getData();
}
this.$route.meta.isBack = false
this.isFirstEnter = false;
},
deactivated() {
console.log("我是第二个页面的 deactivated 方法");
},
destroyed() {
console.log("我是第二个页面的 destroyed 方法");
}
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>

</style>



//page3.vue
<template>
<div class="page3">
<button @click="goBack">返回</button>
<p>{{msg}}</p>
<p>{{str}}</p>
</div>
</template>

<script>
export default {
name: "page3",
data() {
return {
msg: "我是第三个页面",
str: ''
};
},
methods: {
getData() {
console.warn('我调用第三个页面getData方法了')
this.str = "我是通过调用方法加载的数据。。。";
},
goBack() {
this.$router.go(-1)
}
},
beforeRouteEnter(to, from, next) {
console.log('我是第三个页面的beforeRouteEnter方法')
next()
},
created() {
console.log('我是第三个页面的 created 方法')
},
mounted() {
this.getData()
console.log('我是第三个页面的 mounted 方法')
},
activated() {
console.log('我是第三个页面的 activated 方法')
},
deactivated() {
console.log('我是第三个页面的 deactivated 方法')
},
destroyed() {
console.log('我是第三个页面的 destroyed 方法')
}
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>

</style>

依次加入page1,page2,page1

vue-keep-scroll-position

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
(function () {
var plugin;
plugin = function (Vue) {
return Vue.directive('keep-scroll-position', {
bind: function (el) {
el.addEventListener('scroll', function (event) {
event.target.setAttribute('data-vue-keep-scroll-position', event.target.scrollLeft + '-' + event.target.scrollTop);
});
},
inserted: function (el) {
var i, len, pos, target, targets;
targets = el.querySelectorAll('[data-vue-keep-scroll-position]');
if (targets.length > 0) {
for (i = 0, len = targets.length; i < len; i++) {
target = targets[i];
pos = target.getAttribute('data-vue-keep-scroll-position').split('-');
target.scrollLeft = pos[0];
target.scrollTop = pos[1];
}
} else {
if (el.hasAttribute('data-vue-keep-scroll-position')) {
pos = el.getAttribute('data-vue-keep-scroll-position').split('-');
el.scrollLeft = pos[0];
el.scrollTop = pos[1];
}
}
}
});
};
if (typeof Vue !== "undefined" && Vue !== null) {
Vue.use(plugin);
}
if (typeof exports === 'object' && typeof module === 'object') {
return module.exports = plugin;
} else if (typeof define === 'function' && define.amd) {
return define(function () {
return plugin;
});
} else if (typeof window !== "undefined" && window !== null) {
return window.VueKeepScrollPosition = plugin;
}
})();

Install

1
npm i -S vue-keep-scroll-position

Use:

1
2
3
import Vue from 'vue'
import VueKeepScrollPosition from 'vue-keep-scroll-position'
Vue.use VueKeepScrollPosition

只需添加v-keep-scroll-position到您的组件内即可。用于router-view:

1
2
3
<keep-alive>
<router-view v-keep-scroll-position> </router-view>
</keep-alive>

对于简单的动态组件:

1
2
3
<keep-alive>
<component :is="someComponentName" v-keep-scroll-position></component>
</keep-alive>


keep-alive组件实现原理

Vue源码keep-alive.js文件地址

深入keep-alive组件实现

说完了keep-alive组件的使用,我们从源码角度看一下keep-alive组件究竟是如何实现组件的缓存的呢?

  • 首先,你要知道Vue.js内部将DOM节点抽象成了一个个的VNode节点,这个我之前写过相关文章可以参考VNode节点。所以,keep-alive的缓存也是基于VNode节点的而不是直接存储DOM结构。
  • 其实就是将需要缓存的VNode节点保存在this.cache中/在render时,如果VNode的name符合在缓存条件(可以用include以及exclude控制),则会从this.cache中取出之前缓存的VNode实例进行渲染。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
type VNodeCache = { [key: string]: ?VNode };

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

/* 获取组件名称 */
function getComponentName (opts: ? VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}

/* 检测name是否匹配 */
function matches (pattern: string | RegExp, name: string): boolean {
if (typeof pattern === 'string') {
/* 字符串情况,如a,b,c */
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
/* 正则 */
return pattern.test(name)
}
/* istanbul ignore next */
return false
}

/* 修正cache */
function pruneCache (cache: VNodeCache, current: VNode, filter: Function) {
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)) {
if (cachedNode !== current) {
pruneCacheEntry(cachedNode)
}
cache[key] = null
}
}
}
}

/* 销毁vnode对应的组件实例(Vue实例) */
function pruneCacheEntry (vnode: ?VNode) {
if (vnode) {
vnode.componentInstance.$destroy()
}
}

/* keep-alive组件 */
export default {
name: 'keep-alive',
/* 抽象组件 */
abstract: true,

props: {
include: patternTypes,
exclude: patternTypes
},

created () {
/* 缓存对象 */
this.cache = Object.create(null)
},

/* destroyed钩子中销毁所有cache中的组件实例 */
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache[key])
}
},

watch: {
/* 监视include以及exclude,在被修改的时候对cache进行修正 */
include (val: string | RegExp) {
pruneCache(this.cache, this._vnode, name => matches(val, name))
},
exclude (val: string | RegExp) {
pruneCache(this.cache, this._vnode, name => !matches(val, name))
}
},

render () {
/* 得到slot插槽中的第一个组件 */
const vnode: VNode = getFirstComponentChild(this.$slots.default)

const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
/* 获取组件名称,优先获取组件的name字段,否则是组件的tag */
const name: ?string = getComponentName(componentOptions)
/* name不在inlcude中或者在exlude中则直接返回vnode(没有取缓存) */
if (name && (
(this.include && !matches(this.include, name)) ||
(this.exclude && matches(this.exclude, name))
)) {
return vnode
}
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 (this.cache[key]) {
vnode.componentInstance = this.cache[key].componentInstance
} else {
this.cache[key] = vnode
}
/* keepAlive标记位 */
vnode.data.keepAlive = true
}
return vnode
}
}

created与destroyed钩子

created钩子会创建一个cache对象,用来作为缓存容器,保存vnode节点。

1
2
3
4
5
6
7
8
9
type VNodeCache = { [key: string]: ?VNode };    //缓存keep-alive对象VNodeCache

...


created () {
/* 缓存对象 */
this.cache = Object.create(null)
},

destroyed钩子则在组件被销毁的时候清除cache缓存中的所有组件实例。

1
2
3
4
5
6
/* destroyed钩子中销毁所有cache中的组件实例 */
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache[key])
}
},

render

接下来是render函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
render () {
/* 得到slot插槽中的第一个组件 */
const vnode: VNode = getFirstComponentChild(this.$slots.default)

const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
/* 获取组件名称,优先获取组件的name字段,否则是组件的tag */
const name: ?string = getComponentName(componentOptions)
/* name不在inlcude中或者在exlude中则直接返回vnode(没有取缓存) */
if (name && (
(this.include && !matches(this.include, name)) ||
(this.exclude && matches(this.exclude, name))
)) {
return vnode
}
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 (this.cache[key]) {
vnode.componentInstance = this.cache[key].componentInstance
} else {
this.cache[key] = vnode
}
/* keepAlive标记位 */
vnode.data.keepAlive = true
}
return vnode
}

首先通过getFirstComponentChild获取第一个子组件,获取该组件的name(存在组件名则直接使用组件名,否则会使用tag)。接下来会将这个name通过include与exclude属性进行匹配,匹配不成功(说明不需要进行缓存)则不进行任何操作直接返回vnode,vnode是一个VNode类型的对象,不了解VNode的同学可以参考笔者的另一篇文章《VNode节点》 .

1
2
3
4
5
6
7
8
9
10
11
12
/* 检测name是否匹配 */
function matches (pattern: string | RegExp, name: string): boolean {
if (typeof pattern === 'string') {
/* 字符串情况,如a,b,c */
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
/* 正则 */
return pattern.test(name)
}
/* istanbul ignore next */
return false
}

检测include与exclude属性匹配的函数很简单,include与exclude属性支持字符串如”a,b,c”这样组件名以逗号隔开的情况以及正则表达式。matches通过这两种方式分别检测是否匹配当前组件。

1
2
3
4
5
if (this.cache[key]) {
vnode.componentInstance = this.cache[key].componentInstance
} else {
this.cache[key] = vnode
}

接下来的事情很简单,根据key在this.cache中查找,如果存在则说明之前已经缓存过了,直接将缓存的vnode的componentInstance(组件实例)覆盖到目前的vnode上面。否则将vnode存储在cache中。

最后返回vnode(有缓存时该vnode的componentInstance已经被替换成缓存中的了)。

watch

用watch来监听pruneCache与pruneCache这两个属性的改变,在改变的时候修改cache缓存中的缓存数据。

1
2
3
4
5
6
7
8
9
watch: {
/* 监视include以及exclude,在被修改的时候对cache进行修正 */
include (val: string | RegExp) {
pruneCache(this.cache, this._vnode, name => matches(val, name))
},
exclude (val: string | RegExp) {
pruneCache(this.cache, this._vnode, name => !matches(val, name))
}
},

来看一下pruneCache的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 修正cache */
function pruneCache (cache: VNodeCache, current: VNode, filter: Function) {
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)) {
if (cachedNode !== current) {
pruneCacheEntry(cachedNode)
}
cache[key] = null
}
}
}
}

/* 销毁vnode对应的组件实例(Vue实例) */
function pruneCacheEntry (vnode: ?VNode) {
if (vnode) {
vnode.componentInstance.$destroy()
}
}

遍历cache中的所有项,如果不符合filter指定的规则的话,则会执行pruneCacheEntry。pruneCacheEntry则会调用组件实例的$destroy方法来将组件销毁。

最后

Vue.js内部将DOM节点抽象成了一个个的VNode节点,keep-alive组件的缓存也是基于VNode节点的而不是直接存储DOM结构。它将满足条件(pruneCache与pruneCache)的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。

参考链接

彻底揭秘keep-alive原理