此篇文章作为学习keepalive源码的一个记录,如有不对,请多多指教!!也有借鉴其他哥哥姐姐弟弟妹妹们的文章,非常感谢!!
简介
在性能优化上,我们最常见的手段就是缓存。对需要经常访问的资源进行缓存,减少请求或者是初始化的过程,从而降低时间或内存的消耗。
在Vue中就提供了这样一个内置组件- KeepAlive ,它是一个抽象组件,本身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。KeepAlive可用来缓存组件或者路由。把一些不常变动的组件或者需要缓存的组件用KeepAlive包裹起来,这样KeepAlive就会帮我们把组件保存在内存中,而不是直接的销毁,这样做可以保留组件的状态或避免多次重新渲染,以提高页面性能。
用法
KeepAlive可以接收三个属性:
- include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
- exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
- max - 最多可以缓存多少组件。
include 和 exclude 属性允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示
注意:
被KeepAlive 缓存的组件,第一次进入时会触发created、mounted等生命周期函数,当组件再次激活的时候就不会执行 created、mounted 等钩子函数,但是很多业务场景都是希望在被缓存的组件再次被渲染的时候做一些事情,针对这种情况,Vue 提供了activated、deactivated两个钩子函数,它们的执行时机分别是KeepAlive包裹的组件被激活时调用和停用时调用。
LRU缓存策略
我们知道keepAlive是有条件的缓存,既然有限制条件,那么旧的组件需要删除缓存,新的组件就需要加入到最新缓存。keepAlive采用了这样一种策略,LRU(Least recently used,最近最少被使用)策略根据数据的历史访问记录来进行淘汰数据,LRU 策略的设计原则是,如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。keepAlive 缓存机制便是根据LRU策略来设置缓存组件新鲜度,当缓存达到上限时将很久未访问的组件从缓存中删除。
举一个例子
- 现在缓存最大只允许存3个组件(设置max为3),ABC三个组件依次进入缓存,没有任何问题
- 当D组件被访问时,内存空间不足,因为A是最早进入也是最久未被使用的组件,所以A组件从缓存中删除,D组件加入到最新的位置
- 当B组件被再次访问时,由于B还在缓存中,B移动到最新的位置,其他组件相应的往后一位
- 当E组件被访问时,内存空间不足,C变成最久未使用的组件,C组件从缓存中删除,E组件加入到最新的位置
源码解读
import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'
type VNodeCache = { [key: string]: ?VNode };
// 获取组件名
function getComponentName (opts: ?VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}
// 根据自定义的匹配规则匹配组件是否符合缓存条件
function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
return pattern.test(name)
}
return false
}
// prune:修剪
function pruneCache (keepAliveInstance: any, filter: Function) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode: ?VNode = cache[key]
if (cachedNode) {
const name: ?string = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
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
remove(keys, key)
}
const patternTypes: Array<Function> = [String, RegExp, Array]
export default {
name: 'keep-alive',
abstract: true, //抽象组件
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {
// 创建缓存对象与keys数组(储所有组件的key)
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
// 观察include & exclude 值改变了就将匹配的组件缓存 如果值更新后有不需要缓存的组件,就将其销毁并清理缓存
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: VNode = getFirstComponentChild(slot) // 取第一个
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
// 判断是否满足缓存条件
// 组件名与 include 不匹配或与 exclude 匹配都会直接退出并返回 VNode ,不走缓存机制
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)
// 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])
}
}
在组件的开头就将abstract设置为true,代表是该组件是一个抽象组件,不出现在父组件链上是因为在initLifecycle做了处理,判断父级是否为抽象组件,如果是抽象组件,就选取抽象组件的上一级作为父级,忽略与抽象组件和子组件之间的层级关系
export function initLifecycle (vm: Component) {
const options = vm.$options
// 确保了在子组件的$parent属性上能访问到父组件实例,在父组件的$children属性上也能访问子组件的实例
let parent = options.parent
if (parent && !options.abstract) { //存在parent&&当前不是抽象组件
while (parent.$options.abstract && parent.$parent) { // parent是抽象组件&&parent存在parent
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
}
在created钩子函数中创建了一个对象和数组,分别用于缓存组件和存储所有组件的key
created () {
// 创建缓存对象与keys数组(储所有组件的key)
this.cache = Object.create(null)
this.keys = []
},
在KeepAlive销毁的时候调用pruneCacheEntry方法将所有的组件从缓存中清除并销毁,
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
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
remove(keys, key)
}
在mounted中,观察include & exclude, 值改变了就将匹配的组件缓存,如果值更新后有不需要缓存的组件,就将其销毁并清理缓存
mounted () {
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 cachedNode: ?VNode = cache[key]
if (cachedNode) {
const name: ?string = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
keep-alive 没有编写template 模板,而是由 render 函数决定渲染结果。keep-alive 要求同时只有一个子元素被渲染。所以在开头会获取插槽内的子元素,调用 getFirstComponentChild 获取到第一个子元素的 VNode
接着判断当前组件是否符合缓存条件,组件名与 include 不匹配或与 exclude 匹配都会直接退出并返回 VNode ,不走缓存机制。
如果满足匹配条件就会进入下面的缓存机制的逻辑,如果命中缓存,从 cache 中获取缓存的实例设置到当前的组件上,并调整 key 的位置将其放到最后。如果没命中缓存,将当前 VNode 缓存起来,并加入当前组件的 key 。如果缓存组件的数量超出 max 的值,则调用 pruneCacheEntry 将最旧的组件从缓存中删除,即 keys[0] 的组件。之后将组件的 keepAlive 标记为 true ,最后返回vnode
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
? 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)
// 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])
}
其实从代码中我们可以看出keep-alive其实渲染的对象是包裹的子组件。
New Vue -> init -> mount -> compile -> render -> patch -> realdom
组件在 patch 过程会会执行createElm,调用 createComponent 来挂载组件的
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) // initComponent
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
isReactivated 标识组件是否重新激活。在首次加载被KeepAlive包裹的组件时,由KeepAlive中的render函数可知,组件还没有初始化构造完成, componentInstance 还是 undefined 。而组件的 keepAlive 是 true ,因为 KeepAlive作为父级包裹组件,会先于组件挂载,也就是KeepAlive 会先执行 render 的过程,组件被缓存起来,之后对插槽内第一个组件的 keepAlive 赋值为 true ,然后会调用init去初始化组件
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
// 实例、未被销毁、keepalive
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)
}
在第一次组件初始化的时候,componentInstance为undefined,所以会执行到else中的方法,
先去执行createComponentInstanceForVnode,其实就是生成了组件的实例并赋值到 componentInstance ,随后调用 $mount 挂载组件,回到 createComponent 中,继续执行下面的逻辑,这时候componentInstance已经存在,调用 initComponent 将 vnode.elm 赋值为真实dom,然后调用 insert 将组件的真实dom插入到父元素中。所以在初始化渲染中,KeepAlive将组件缓存起来,然后正常的渲染组件。
keep-alive包裹的组件是如何使用缓存的呢?
当组件命中缓存被重新激活。再次经历 patch 过程, KeepAlive 是根据插槽获取当前的组件,那么插槽的内容又是如何更新实现缓存的呢
在非初始化渲染时, patch 会调用 patchVnode 对比新旧节点进行更新。在patchVnode中有这样一段操作
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
调用了componentVNodeHooks中的prepatch方法
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},
//在 lifeCycle文件中
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
const newScopedSlots = parentVnode.data.scopedSlots
const oldScopedSlots = vm.$scopedSlots
const hasDynamicScopedSlot = !!(
(newScopedSlots && !newScopedSlots.$stable) ||
(oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
(newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)
)
// Any static slot children from the parent may have changed during parent's
// update. Dynamic scoped slots may also have changed. In such cases, a forced
// update is necessary to ensure correctness.
const needsForceUpdate = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
hasDynamicScopedSlot
)
// resolve slots + force update if has children
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
}
从注释中可以看到 needsForceUpdate 是有插槽才会为 true , keep-alive 符合条件。首先调用 resolveSlots 更新keepAlive 的插槽,然后调用 f o r c e U p d a t e 让 k e e p A l i v e 重 新 渲 染 , 再 走 一 遍 r e n d e r 。 因 为 组 件 在 初 始 化 已 经 缓 存 了 , k e e p − a l i v e 直 接 返 回 缓 存 好 的 A 组 件 V N o d e 。 V N o d e 准 备 好 后 , 又 来 到 了 p a t c h 阶 段 。 这 时 候 组 件 再 次 经 历 c r e a t e C o m p o n e n t 的 过 程 , 调 用 i n i t 。 因 为 c o m p o n e n t I n s t a n c e 已 经 存 在 并 且 k e e p A l i v e 属 性 为 t r u e , 所 以 不 会 再 去 执 行 forceUpdate 让keepAlive重新渲染,再走一遍 render 。因为组件在初始化已经缓存了, keep-alive 直接返回缓存好的A组件 VNode 。VNode 准备好后,又来到了 patch 阶段。这时候组件再次经历 createComponent 的过程,调用 init 。因为componentInstance已经存在并且keepAlive属性为true,所以不会再去执行 forceUpdate让keepAlive重新渲染,再走一遍render。因为组件在初始化已经缓存了,keep−alive直接返回缓存好的A组件VNode。VNode准备好后,又来到了patch阶段。这时候组件再次经历createComponent的过程,调用init。因为componentInstance已经存在并且keepAlive属性为true,所以不会再去执行mount,所以当组件重新激活的时候不会再执行生命周期钩子,而是走到了prepatch更新实例,回到 createComponent ,此时的 isReactivated 为 true ,调用 reactivateComponent 方法
function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
// 中间省略,最后调用insert方法
insert(parentElm, vnode.elm, refElm)
}
在reactivateComponent方法总,调用insert插入组件的dom节点,至此缓存渲染流程完成
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
总结:
组件首次渲染时,keepAlive会将组件缓存起来。等到缓存的组件重新渲染时,keepAlive 会调用$forceUpdate 重新渲染。这样在 render 时就获取到最新的组件,如果命中缓存则从缓存中返回 VNode 。这样就实现了keepAlive的缓存功能
那么KeepAlive的钩子函数(activated、deactivated)是怎么执行的呢?
上面的渲染流程中已经说过,在组件渲染的时候会调用patch方法,在patch方法的最后会调用invokeInsertHook
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
在invokeInsertHook里执行会去执行insert,在insert中判断了vnode.data.keepAlive,如果为true执行内部的逻辑,无论怎样最后都会去执行activateChildComponent,最终调用了activated钩子函数
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
deactivated钩子也是在patch中调用的
// 在patch中
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
function invokeDestroyHook (vnode) {
let i, j
const data = vnode.data
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
}
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
}
}
// 在create-component中
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
// 在lifecycle中
export function deactivateChildComponent (vm: Component, direct?: boolean) {
if (direct) {
vm._directInactive = true
if (isInInactiveTree(vm)) {
return
}
}
if (!vm._inactive) {
vm._inactive = true
for (let i = 0; i < vm.$children.length; i++) {
deactivateChildComponent(vm.$children[i])
}
callHook(vm, 'deactivated')
}
}