我们知道,keep-alive是vue中抽象的内置的组件,它自身不会渲染一个 DOM
元素,也不会出现在父组件链中。当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。也就是说把一些不常变动的组件或者需要缓存的组件用
包裹起来,这样
就会帮我们把组件保存在内存中,而不是直接的销毁,这样做可以保留组件的状态或避免多次重新渲染,以提高页面性能。
举个例子来说,当我们在切换一个页签的时候,希望切换的内容不会发生变化,这也就是意味着组件并没有被销毁,而是被保留下来了,那么这个时候就会用到keep-alive。那么具体keep-alive的实现原理是什么呢?
用法:
keep-alive接收三个属性:
include
- 字符串或正则表达式。只有名称匹配的组件会被缓存。exclude
- 字符串或正则表达式。任何名称匹配的组件都不会被缓存。max
- 数字。最多可以缓存多少组件实例。include
和 exclude
属性允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:
匹配时首先检查组件自身的 name
选项,如果 name
选项不可用,则匹配它的局部注册名称 (父组件 components
选项的键值),也就是组件的标签值。匿名组件不能被匹配。
max
表示最多可以缓存多少组件实例。一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉。
大致keep-alive的用法就是这样,接下来,我们来具体来看看实现的原理。
原理:src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true,
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},
created () {
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
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
const key = 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
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)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
该组件内没有常规的标签,取而代之的是它内部多了一个叫做
render
的函数,所以它不是一个常规的模板组件,而是一个函数式组件。执行
组件渲染的时候,就会执行到这个 render
函数。
我们可以看到从props传递进来的就是三个属:include
、exclude
和max。
include
表示只有匹配到的组件会被缓存,而 exclude
表示任何匹配到的组件都不会被缓存, max
表示缓存组件的数量,因为我们是缓存的 vnode
对象,它也会持有 DOM,当我们缓存的组件很多的时候,会比较占用内存,所以该配置允许我们指定缓存组件的数量。
在created阶段,初始化了俩个变量this.cache
和 this.keys.this.cache
是一个对象,用来存储需要缓存的组件.this.keys
是一个数组,用来存储每个需要缓存的组件的key
,即对应this.cache
对象中的键值。
当
组件被销毁时,此时会调用destroyed
钩子函数,在该钩子函数里会遍历this.cache
对象,然后将那些被缓存的并且当前没有处于被渲染状态的组件都销毁掉并将其从this.cache
对象中剔除。
// pruneCacheEntry函数
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
钩子函数中观测 include
和 exclude
的变化。如果include
或exclude
发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache
函数。
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)
}
在该函数内对this.cache
对象进行遍历,取出每一项的name
值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry
函数将这个已经不需要缓存的组件实例先销毁掉,然后再将其从this.cache
对象中剔除。
结下来就是重头戏render函数。在render
函数中首先获取第一个子组件节点的 vnode。
然后用组件名称跟 include
、exclude
中的匹配规则去匹配。如果组件名称与 include
规则不匹配或者与 exclude
规则匹配,则表示不缓存该组件,直接返回这个组件的 vnode
,否则的话走下一步缓存。首先获取组件的key
值。拿到key
值后去this.cache
对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存。直接从缓存中拿 vnode
的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys
中最后一个。如果this.cache
对象中没有该key
值。表明该组件还没有被缓存过,则以该组件的key
为键,组件vnode
为值,将其存入this.cache
中,并且把key
存入this.keys
中。此时再判断this.keys
中缓存组件的数量是否超过了设置的最大缓存数量值this.max
,如果超过了,则把第一个缓存组件删掉。
为什么要删除第一个缓存组件并且为什么命中缓存了还要调整组件key的顺序?
这其实应用了一个缓存淘汰策略LRU:LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
this.keys
中;this.keys
的尾部;this.keys
满的时候,将头部的数据丢弃;LRU的核心思想是如果数据最近被访问过,那么将来被访问的几率也更高,所以我们将命中缓存的组件key
重新插入到this.keys
的尾部,这样一来,this.keys
中越往头部的数据即将来被访问几率越低,所以当缓存数量达到最大值时,我们就删除将来被访问几率最低的数据,即this.keys
中第一个缓存的组件。这也就之前加粗强调的已缓存组件中最久没有被访问的实例会被销毁掉的原因所在。
以上工作做完后设置 vnode.data.keepAlive = true
,最后将vnode
返回。这就是render函数的整个过程。
生命钩子函数
组件一旦被
缓存,那么再次渲染的时候就不会执行 created
、mounted
等钩子函数,但是我们很多业务场景都是希望在我们被缓存的组件再次被渲染的时候做一些事情,好在Vue
提供了 activated
和deactivated
两个钩子函数,它的执行时机是
包裹的组件激活时调用和停用时调用,下面我们就通过一个简单的例子来演示一下这两个钩子函数。
let A = {
template: '' +
'A Comp
' +
'',
name: 'A',
mounted(){
console.log('Comp A mounted')
},
activated(){
console.log('Comp A activated')
},
deactivated(){
console.log('Comp A deactivated')
}
}
let B = {
template: '' +
'B Comp
' +
'',
name: 'B',
mounted(){
console.log('Comp B mounted')
},
activated(){
console.log('Comp B activated')
},
deactivated(){
console.log('Comp B deactivated')
}
}
let vm = new Vue({
el: '##app',
template: '' +
'' +
'' +
' ' +
' ' +
'' +
'',
data: {
currentComp: 'A'
},
methods: {
change() {
this.currentComp = this.currentComp === 'A' ? 'B' : 'A'
}
},
components: {
A,
B
}
})
我们定义了两个组件A
和B
并为其绑定了钩子函数,并且在根组件中用
组件包裹了一个动态组件,这个动态组件默认指向组件A
,当点击switch
按钮时,动态切换组件A
和B
。
当第一次打开页面时,组件A
被挂载,执行了组件A
的mounted
和activated
钩子函数,当点击switch
按钮后,组件A
停止调用,同时组件B
被挂载,此时执行了组件A
的deactivated
和组件B
的mounted
和activated
钩子函数。此时再点击switch
按钮,组件B
停止调用,组件A
被再次激活,我们发现现在只执行了组件A
的activated
钩子函数,这就验证了文档中所说的组件一旦被
缓存,那么再次渲染的时候就不会执行 created
、mounted
等钩子函数。