用法回顾
通过keep-alive
包裹的组件,可以帮助我们将一些不常变动
或者需要缓存内容
的组件保存在内存中,保留组件的状态,避免多次渲染,提高页面性能。
vue
<!-- 父页面 -->
<template>
<div>
<button @click="switchName('Child1')">组件1</button>
<button @click="switchName('Child2')">组件2</button>
<component :is="componentName"></component>
</div>
</template>
<script>
// 引入子组件
import Child1 from "./Child1.vue";
import Child2 from "./Child2.vue";
export default {
components: {
Child1,
Child2,
},
data() {
return {
componentName:'Child1'
};
},
methods:{
switchName(componentName){
this.componentName = componentName
}
}
};
</script>
<!-- 子页面1 -->
<template>
<div>
<input type="text">
</div>
</template>
<!-- 子页面2 -->
<template>
<div>
<input type="text">
</div>
</template>
上面代码实现的效果如下:
可以看出,我们在组件1和组件2中分别输入了内容,但是切换了组件,以前输入的内容就不存在了,说明切换组件把之前的组件销毁了,当我们需要保存之前的内容时,我们需要用keep-alive
将组件包裹起来。
vue
<keep-alive>
<component :is="componentName"></component>
</keep-alive>
接收参数
keep-alive组件支持接收3个参数,分别是:
include
字符串、数组、或正则表达式,名称匹配的组件会被缓存exclude
字符串、数组、或正则表达式,名称匹配的组件不会被缓存max
数字,最大支持缓存页面个数
代码示例如下:
vue
<!-- 字符串 -->
<keep-alive include="Child1,Child2">
<component :is="componentName"></component>
</keep-alive>
<!-- 正则 -->
<keep-alive :include="/Child1|Child2/">
<component :is="componentName"></component>
</keep-alive>
<!-- 数组 -->
<keep-alive :include="['Child1','Child2']">
<component :is="componentName"></component>
</keep-alive>
vue
<!-- 最大缓存组件数 -->
<keep-alive :max="5">
<component :is="componentName"></component>
</keep-alive>
实现原理
js
export default {
name: 'keep-alive',
abstract: true, //表示抽象组件,判断组件是否需要渲染成真是的dom
// 组件接收的参数
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},
// 初始化定义两个属性:cache、keys
created() {
this.cache = Object.create(null)
this.keys = []
},
// 组件销毁时
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
// 观测incluede、exclude变化
mounted() {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render() {
/* 获取默认插槽中的第一个组件节点 */
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
/* 获取该组件节点的componentOptions */
const componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
/* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
const name = getComponentName(componentOptions)
const { include, exclude } = this
/* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
if (
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
// 以下走缓存
const { cache, keys } = this
// 获取组件的key值
const key = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
//如果命中缓存
if (cache[key]) {
// 直接从缓存中拿 vnode 的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一个
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key)
} else {
//没有命中缓存,将一下组件缓存
cache[key] = vnode
keys.push(key)
// 如果长度超过了max 则删除第一个
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])
}
}
我们来分析下以上源码:
created
在created中,定义了cache
和keys
,cache
是一个对象,用来储存需要缓存组件的信息,格式如下:
js
this.cache = {
'key1':'组件1',
'key2':'组件2'
}
this.keys
是一个数组,储存需要缓存组件的key,即this.cache
对应的键。
destroyed
<keep-alive>
组件销毁时,会调用destory
,遍历this.cache
对象,将组件销毁,并将销毁的组件从this.cache
中删除:
js
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
}
//组件销毁函数
function pruneCacheEntry (cache,key,keys,current) {
const cached = cache[key]
//判断当前没有处于被渲染状态的组件,将其销毁
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
mounted
js
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}
监听include、exclude
的变化,如果变化了,表示缓存规则发生了改变,就调用pruneCache
函数:
js
function pruneCache (keepAliveInstance, filter) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode = cache[key]
if (cachedNode) {
const name = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
//组件销毁函数
function pruneCacheEntry (cache,key,keys,current) {
const cached = cache[key]
//判断当前没有处于被渲染状态的组件,将其销毁
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
pruneCache
函数对this.cache
遍历,取出每一项的name
,用这个name
与新的缓存规则进行匹配,如果匹配不上,表示新的缓存规则下,该组件已经不需要缓存,需要将组件实例销毁掉,并将其在thsi.cache
中删除,即调用pruneCacheEntry
函数。
render
js
render() {
/* 获取默认插槽中的第一个组件节点 */
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
/* 获取该组件节点的componentOptions */
const componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
/* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
const name = getComponentName(componentOptions)
const { include, exclude } = this
/* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
if (
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
// 以下走缓存
const { cache, keys } = this
// 获取组件的key值
const key = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
//如果命中缓存
if (cache[key]) {
// 直接从缓存中拿 vnode 的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一个
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key)
} else {
//没有命中缓存,将组件缓存
cache[key] = vnode
keys.push(key)
// 如果长度超过了max 则删除第一个
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
//设置keepAlive标记位,缓存组件的钩子函数要用到
vnode.data.keepAlive = true
}
//将vnode返回
return vnode || (slot && slot[0])
}
render函数主要做了几件事:
- 1.获取第一个组件和组件名(keep-alive只处理第一个子元素)
- 2.组件
name
不在inlcude
中或者存在于exlude
中则表示不缓存,直接返回vnode
- 3.生成组件
key
,如果key
在cache
中存在,说明已经缓存过该组件,直接从缓存中拿vnode
的组件实例,此时重新调整该组件key
的顺序,将其从原来的地方删掉并重新放在this.keys
中最后一个。 - 4.如果不在,表明该组件还没有被缓存过,将该组件的
key
为键,组件vnode
为值,将其存入this.cache
中,并且把key存入this.keys
中。此时再判断this.keys
中缓存组件的数量是否超过了最大缓存数量值this.max
,如果超过了,则把第一个缓存组件删掉。
包裹的组件渲染
js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
// 将缓存的DOM(vnode.elm)插入父元素中
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
初始时,在keep-alive
的render
函数中,将包裹的组件添加到chche
中,此时,vnode.componentInstance
的值为undefined
,未执行if (isDef(vnode.componentInstance))
,再次访问包裹组件时,vnode.componentInstance
的值就是已经缓存的组件实例,执行insert(parentElm, vnode.elm, refElm)
,将vnode.elm
插入到父元素中,从而显示在页面。
即对应
js
//如果命中缓存
if (cache[key]) {
//再次激活时获取vnode.componentInstance
vnode.componentInstance = cache[key].componentInstance
...
} else {
//初始化时先缓存
cache[key] = vnode
keys.push(key)
...
}