基本用法
<keep-alive>
- <div>123</div>
+ <component :is="switchOne ? 'TabOne' : 'TabTwo'" />
- <component :is="switchOne ? 'TabTwo' : 'TabOne'" />
</keep-alive>
- keep-alive 只会去缓存 第一级 的 第一个 组件
- 我们可以通过
include
/exclude
/max
来对缓存进行更小颗粒的控制 - 缓存的组件在切换的时候会触发
activated
/deactivated
的生命周期函数
组件实现原理
export default {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {
// 创建缓存队列以及其对应的 key 值队列
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
// 销毁所有已经保存的组件实例
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
// 监听 include / exclued,用于让缓存队列保持一致性
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render () {
// 从 $slot 中获取 keep-alive 的 children
const slot = this.$slots.default
// 找到第一个组件的最新的 VNode
const vnode: VNode = getFirstComponentChild(slot)
// 获取其配置信息
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// 被捕获的组件名称
const name: ?string = getComponentName(componentOptions)
// 如果不在缓存的目标范围内,则直接返回新的 VNode
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
// 获得组件对应的 key 值
const key: ?string = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
// LRU 缓存操作,如果缓存中存在对应的实例,则将实例注入到 VNode 中
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)
}
}
// 标注 keepAlive,方便 Vue 配合
vnode.data.keepAlive = true
}
// 返回
return vnode || (slot && slot[0])
}
}
缓存核心操作的内容其实是 componentInstance
,如果已经初始化过对应的实例则不用初始化,直接取出即可。
Vue 内部是如何配合的
先明确一下正常组件实例化的过程:
- AST(编译 template)
- CodeGen(生成 render 函数)
- VNode(通过 render 获得组件 VNode,并将组件的构造函数存到 VNode 中)
- Patch
- 新增节点(通过 VNode 中的构造函数初始化组件)
- 更新节点(基于旧的组件实例更新其状态)
- 删除节点
如果没有 keep-alive
,组件在进行切换到时候,Vue 在 patch 会认为是新增的节点而重新创建组件实例,从而导致先前可能存在状态会被清空。
如果有 keep-alive
,切换节点操作最后 patch 还是会走到 新增节点 **的逻辑,但是不一样的是,新的 VNode 中已经有组件实例,同时 VNode.data.keepAlive
标识了是一个 keep-alive
下的组件。
下面是新增组件的函数
- 这里主要是调用 VNode 的 init()
同时实例化完后会决定是否调用重新激活(activated)生命周期函数
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// 标记是否需要重新激活
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
// 调用 init 钩子函数
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// 如果组件实例构建成功
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
// 如果是重新激活的组件则调用对应的钩子函数
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
这里是 keep-alive 与普通组件初始化开始走向分岔路的起点
如果是 keep-alive 下的组件,同时初始化过了,则直接 patch
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// 如果已经有组件实例 并且 没被销毁 并且有 keepAlive 则走 patch
// 反之走初始化
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},