基本使用
<keep-alive>
<router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" />
如果我们想缓存一个页面或者组件,将它放置在 keep-alive
组件中,就能神奇地实现缓存组件实例。例如:页面的滚动位置、输入框内的内容在切换的过程中都不会丢失。
平时使用就当作是一个黑盒子一样,keep-alive 帮我们完成缓存的需求,今天来看下它的到底是怎么实现这一功能的。
源码分析
自我实现代码思路
在打开源码前试想下如果是自己,该如何去缓存我们页面状态呢。来让我自己先设计下。
页面都是有一个或者多个组件构成的,也就是我们常写的 .vue
文件。
<template>
<div>{{ msg }}</div>
</template>
<script>
export default {
data() {
msg: 'hello'
}
}
</script>
看下它是如何转化渲染成最终的 DOM 的
现在我们来看下 keep-alive
的页面跳转的表现,例如从 A -> B 页面跳转,然后从 B -> A 返回 A 页面的状态还保存。
从页面 dom 处分析:
- A -> B 时候 A 页面相关的 DOM 是卸载了,所以排除通过 css 来隐藏 A 页面 DOM 的情况。所以 B -> A 的时候肯定是经历了 patch DOM 的过程。
- 所以我们只能在 vnode 这层做文章,我们需要保存 A 页面的 vnode, 因为这个 vnode 包含了关于页面 A 的状态,当页面再次加载时把之前的状态都重新渲染出来。
源码
我看的是 Vue 2.6.12版本的源码,打开 vue/src/core/components/keep-alive.js
, 虽然我们前面梳理了下大概的思路,当我们打开 keep-alive.js
文件惊奇发现代码只有 124行,而且代码看着很清晰。
来看下主流程吧
初始化 cache
和 keys
变量,用于缓存和标记组件用。
created () {
this.cache = Object.create(null)
this.keys = []
},
核心逻辑
render(){
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 {
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])
}
- 首先我们来看下通过
cache[key]
判断组件是否有缓存,这个 key 是组件的唯一标识,我们暂时认为通过它来拿到组件就行,后序可以看看它是如何获取这个标识的。 - 当我们首次访问组件时候,
cache[key]
肯定为 undefined 走的下面的逻辑,此时我们将组件的 vnode 保存在 cache 的唯一 key 下。并且将 key push 到 keys 中,keys 是记录组件最近活跃组件的
,当我们设置max
最大缓存组件数时候需要通过它来判断,活跃最久远的组件销毁。
- pruneCacheEntry 的逻辑暂时不关注,它是额外的功能。我们目前只关心如何缓存组件。
经过上面的两步,已经将组件对应的 vnode
缓存到 cache 对象中,并且更新了活跃组件列表 keys
。此时相当于访问了 A 页面。
如果我们从 B -> A 再次跳转回 A 页面时候,我们就回走上面的逻辑。
- 此时并没有直接返回缓存到 vnode, 而是将缓存实例
cache[key].componentInstance
添加到 vnode 对象上。
- vnode.componentInstance 对象是只有渲染过的组件才存在,第一次进入或者没有缓存的组件的 vnode.componentInstance 是为 undefined
- 后面的逻辑还是跟首次访问一样,设置 vnode.data.keepAlive=true 最终返回 vnode
到这里就完成缓存组件再次访问时拿到 vnode 的过程,至于如何去渲染缓存的 vnode,我们继续往下看。
keep-alive patch 流程
有了 vnode 后,需要将它转化为 dom, patch 流程在
core/vdom/patch.js
中
渲染 keep-alive 组件,对应的流程是
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
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 */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
- isReactivated 对应的就是在
keep-alive
render 函数中的 vnode.componentInstance 和 vnode.data.keepAlive=true 设置。 - 走到 initComponent(vnode, insertedVnodeQueue),拿到
vnode.componentInstance.$el
缓存的 DOM 元素 - reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) 将 dom 挂在到 parentElm 上。
到此完成了 keep-alive 组件的渲染。
keep-alive 的其他用法实现
keep-alive 还接受其他 props。
include
- 字符串或正则表达式。只有名称匹配的组件会被缓存。exclude
- 字符串或正则表达式。任何名称匹配的组件都不会被缓存。max
- 数字。最多可以缓存多少组件实例。
拿 include 的实现作为举例:
export default {
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
mounted () {
// 发现 include 发生变化后,执行 pruneCache 移除不符合规则的组件缓存
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
}
}
来看下 pruneCahe 函数的实现
function pruneCache (keepAliveInstance: any, filter: Function) {
// keepAliveInstance 就是传递 this,也就是 keep-alive 组件
const { cache, keys, _vnode } = keepAliveInstance
// 遍历缓存组件
for (const key in cache) {
const cachedNode: ?VNode = cache[key]
if (cachedNode) {
// 获取组件名,也就是我们在组件中定义的 name 选项
const name: ?string = getComponentName(cachedNode.componentOptions)
/**
* 执行 matches 函数判断,组件名是否符合 include 的规则
* 如果不符合,则执行 pruneCacheEntry 移除缓存组件
*/
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
来看下 pruneCacheEntry 函数实现
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()
}
// 移除缓存组件
cache[key] = null
// 移除 keys 对应的活跃组件
remove(keys, key)
}
总结
keep-alive 组件的流程判断如下,当拿到 keep-alive 组件的 vnode 时然后进行 patch 挂载 DOM 流程。