在上一篇中,介绍了keep-alive的作用以及应用
https://blog.csdn.net/m0_56698268/article/details/129713779
这里稍微看一看它的源码是如何实现的
在源码文件VUE-DEV的src/core/components/keep-alive.js中
export default {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
methods: {
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this
if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache
cache[keyToCache] = {
name: getComponentName(componentOptions),
tag,
componentInstance,
}
keys.push(keyToCache)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
}
},
created () {
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
this.cacheVNode()
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
updated () {
this.cacheVNode()
},
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
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
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
// delay setting the cache until update
this.vnodeToCache = vnode
this.keyToCache = key
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
我们可以发现,它就是和组件的写法是差不多的,但是不同的在于,它是抽象组件
(第3行 abstract : true), 抽象组件的意思就是 keep-alive组件 不会像其他组件一样渲染出DOM节点,但是会渲染出Vnode虚拟节点,所以在Vnode Tree上是可以找到keep-alive的虚拟节点的,DOM Tree上找不到。
接着往下,是它的props属性
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
include和exclude是用于名称匹配的组件是否会被缓存的,max是用于控制可以缓存多少组件实例的,如果一旦超过最大值,会把末尾最后用到的组件进行销毁
然后是一个创建虚拟节点的方法,先不管它。再往下就是五个钩子函数 created、destroyed、mounted、updated、render
created () {
this.cache = Object.create(null)
this.keys = []
},
创建时,会给实例注入两个属性,cache和keys。
cache是一个对象,里面会存储我们要缓存的组件
keys会储存cache对象的key的集合。
所以,我们就可以通过这两个属性获取到当前缓存的哪些组件
destroyed:销毁的时候,需要把cache对象里面把缓存的组件全部删掉,循环遍历cache里面的值,调用pruneCacheEntry
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
function pruneCacheEntry (
cache: CacheEntryMap,
key: string,
keys: Array,
current?: VNode
) {
const entry: ?CacheEntry = cache[key]
if (entry && (!current || entry.tag !== current.tag)) {
entry.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
mouted: 逻辑也很简单,就是监听 include和exclude,一旦发生改变,那肯定要重新匹配,过滤下cache
mounted () {
this.cacheVNode()
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
function pruneCache (keepAliveInstance: any, filter: Function) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const entry: ?CacheEntry = cache[key]
if (entry) {
const name: ?string = entry.name
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
updated () {
this.cacheVNode()
},
这个没啥好讲的
重点是这个render,keep-alive实现缓存的核心代码就在这个钩子函数里
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
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
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
/* 调整该组件key的顺序,将其从原来的地方删掉并重新放在最后一个 */
remove(keys, key)
keys.push(key)
}
else {
cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
首先是获取到默认插槽里的内容,然后调用getFirstComponentChild方法获取第一个子组件
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
假设现在keep-alive组件里的这样子的
123123
那么它获取的应该是componentA组件 (123123并不是组件)
export function getFirstComponentChild (children: ?Array): ?VNode {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const c = children[i]
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c
}
}
}
}
这里就是判断c是不是组件,一旦c是组件就返回
接着的逻辑就是 如果vnode存在并获取它的组件选项componentOptions,如果组件选项存在,进入下一步,如果不存在,直接返回vnode或者slot[0]
if (componentOptions) {
......
}
return vnode || (slot && slot[0])
所以,直接看组件选项存在后的执行就行了
首先就是取组件的名称去和include和exclude去进行匹配(如果有name的话),如果没有name,就会取tag名。
// check pattern
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
}
function getComponentName (opts: ?VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}
如果没有匹配上,直接返回这个组件的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
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key)
}
else {
cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
上面的代码:
首先是看这个vnode有没有key,有的话,直接保存,没有的话,会用“cid::组件的tag”来代替
然后就是看cache中有没有该组件实例,如果有的话,直接取出这个组件实例给vnode
这里的意思是什么呢?比如就是已经缓存了A和B两个组件,此时A是失活状态的,但我切换到A组件时,它reactive了,那么因为已经缓存过了,那我直接取cache里缓存过的这个A直接赋给vnode,就可以实现reactive了。
就这就是把keys这个数组进行一个刷新,具体是怎么操作的呢?
其实用到的是操作系统里内存管理的一个LRU置换算法,全称叫做 Least Recently Used,叫做最近最少使用。他会根据数据的历史访问记录来淘汰数据,这种算法的核心思想怎么来的呢? 它是这么认为的,如果某一个数据最近被访问过了,它会认为它将来被访问的几率就会变高,而如果有的数据最近很少访问或者没有访问,那将来访问它的几率就不会高
用一个简单的例子解释一下,现在有一个cache,但是它只有三个块,也就是只能装下3个页面,而我现在要依次访问如下的页面: 页面1、页面2、页面3、页面1、页面4
一开始,前三个页面可以按照顺序依次存在放块A、B、C中,而到了第四个页面为页面1,首先它会查看页面1在不在块内,发现确实在,在块A中。OK,那我就不用置换掉其他页面了,此时会调整访问顺序,之前的顺序是 1、2、3的顺序访问,第四个页面进来以后,他们访问的先后顺序就调整为了 2、3、1 (也就代表页面1是最近才被访问的)。
那再接着,页面4要被访问了,首先看cache里有页面4吗? 没有! 那我需要先置换出一个页面,空出位置给我的页面4。 那要置换掉谁呢? 就看谁是LRU了,按照上面更改后的访问顺序(2、3、1),发现页面2是最久远被访问的,所以置换掉页面2,从而放入页面4。同样的,依旧会刷新他们的访问顺序,现在就是(3、1、4)了
再次基础上,如果下一个要访问的页面是页面5呢?存放的块是哪个?且置换后的顺序为?
存在在块C中,顺序为(1、4、5)
这就是简单的LRU算法的小例子,但是这种算法有一个缺点会导致页面的抖动(当然,这里就不要求掌握了,感兴趣的可以去看看其他内存管理的置换算法)
以上的例子就能很好地解释源码中的if else的逻辑。
最后标识vnode.data.keepAlive=true
Last But Not Least,组件一旦被keep-alive缓存,那么再次渲染时就不会执行created、mouted、destroyed钩子,使用keep-alive组件后,被缓存的组件生命周期会多activated和deactivated两个钩子函数。