系列文章目录
vue源码学习——初始化data
vue源码学习——响应式数据
前言
在《vue keep-alive 缓存 不生效解决方案》已经介绍了vue keep-alive的使用经验,这节主要介绍源码实现部分
一、源码位置
src/core/components/keep-alive.js
二、源码分析
1.几个重要参数
name: 'keep-alive'
// 组件名称,abstract: true
// 表示抽象组件,不会被渲染到真实DOM中,也不会出现在父组件链中。props
: {
include: patternTypes, // 字符串或正则表达式。只有名称匹配的组件会被缓存。
exclude: patternTypes, // 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
max: [String, Number] // 数字。最多可以缓存多少组件实例。一旦数字达到了,在新实例被 创建之前,已缓存组件中最久没有被访问的实例会被销毁掉。
},
props传参用法示例-include:
<keep-alive include="a">
<component></component>
</keep-alive>
2.创建与销毁
- 创建:定义了2个对象,一个是cache对象用于存储组件实例对象,一个keys数组,存储缓存的组件的key。
created () {
this.cache = Object.create(null) // 缓存组件对象
this.keys = [] // 存储组件的Key,根据LRU策略来调整缓存组
},
- 销毁:遍历销毁所有的组件缓存
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
}
销毁阶段调用的pruneCacheEntry(this.cache, key, this.keys)
,将组件实例,调用自身detroy钩子函数,并且从缓存中移除。
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy() // 调用组件 $destroy 方法销毁组件实例
}
cache[key] = null // this.cache[key]置空
remove(keys, key) // 缓存组件置空,并移除对应的 key
}
3、渲染
render函数最终返回keep-alive标签内部包裹的组件的vnode对象,决定渲染结果。
3.1主要做了什么
- 获取第一个插槽组件vnode
- 通过inclue,exclude等判断是否需要缓存,若否走3, 若命中走4
- 不需要缓存,直接返回vnode,不走缓存
- 需要缓存,已存在cache[key]走5, 否则走6
- 已存在该组件缓存,获取缓存的组件实例cache[key].componentInstance,并设置到当前的组件上,并调整 key 的位置将其放到最后。(make current key freshest)
- 若不存在该组件缓存,将当前vnode缓存起来,并填充cache[key] =vnode;keys.push(key).若缓存个数超过了this.max的值,即预设缓存空间不足,就抛弃删除最旧的组件(key[0]对应的);
- 标记组件为已缓存的组件:vnode.data.keepAlive = true
- 最终返回return vnode || (slot && slot[0])
在第6点中,当预设缓存空间不足是,采用了的 LRU
缓存淘汰策略(LRU 算法【最近最少使用】)
3.2 LRU(Least Recently Used )
LRU的基本思想:(最近最少使用)
最近使用的页面数据会在未来一段时期内仍然被使用,已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用。
基于这个思想,会存在一种缓存淘汰机制,每次从内存中找到最久未使用的数据然后置换出来,从而存入新的数据!
参考下图帮助理解vue keep-alive利用LRU缓存组件的思想:
附完整源码
export default {
name: 'keep-alive',
abstract: true, // <keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。
props: { // 允许有条件地缓存
include: patternTypes, // 字符串或正则表达式。只有名称匹配的组件会被缓存。
exclude: patternTypes, // 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
max: [String, Number] // 数字。最多可以缓存多少组件实例。一旦数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉。
},
created () {
this.cache = Object.create(null) // 缓存组件对象
this.keys = [] // 存储组件的Key,根据LRU策略来调整缓存组
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted() {
// 监听 include 和 exclude 的变化,属性发生改变时调整缓存和 keys 的顺序,最终调用的也是 pruneCacheEntry。
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
// render函数决定渲染结果,最终返回组件的Vnode.因此keep-alive 并非真的不会渲染,而是渲染的对象是包裹的子组件。
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot) // 若keep-alive存在多个子元素,要求同时只有一个子元素被渲染。所以在开头会获取插槽内的子元素,调用 getFirstComponentChild 获取到第一个子元素的 VNode。
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// -------start-----------
// 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
}
//-------end-----------以上判断当前组件是否符合缓存条件,组件名与include不匹配或与exclude匹配都会直接退出并返回 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
// 如果命中缓存,从 cache 中获取缓存的实例设置到当前的组件上,并调整 key 的位置将其放到最后。
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {// 如果没命中缓存,将当前 VNode 缓存起来,并加入当前组件的 key。
cache[key] = vnode
keys.push(key)
// prune oldest entry
//如果缓存组件的数量超出 max 的值,即缓存空间不足,则调用 pruneCacheEntry 将最旧的组件从缓存中删除,即 keys[0] 的组件。
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
// 之后将组件的 keepAlive 标记为 true,表示它是被缓存的组件。
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}