keep-alive
用法
- include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
- exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
- max - 数字。最多可以缓存多少组件实例。
<keep-alive include='myname' exclude='exmyname' :max='10'>
<comp-a/>
</keep-alive>
_getComponentName
- 判断是否传入组件实例 获取组件的属性值 name 如果为存在组件属性则使用组件注册的标签来命名缓存的 key
function _getComponentName(opts?: VNodeComponentOptions): string | null {
return opts && (getComponentName(opts.Ctor.options as any) || opts.tag)
}
matches
- 判断当前 include 和 exclude 的类型 这两个属性只支持 Object | String | Array 类型
function matches(
pattern: string | RegExp | Array<string>,
name: string
): boolean {
if (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
}
pruneCache
- pruneCache 删除缓存接受一个 keepAlive实例和一个filter函数,首先从实例中取出 cache、keys和对应的 vnode。然后遍历整个 cache 对象,如果当前组件实例在缓存中并且参数合法,就执行 pruneCacheEntry 方法
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)
}
}
}
}
mounted
- mounted 生命周期 对props 参数进行了 侦听处理 里面调用了 pruneCache 方法
mounted () {
this.cacheVNode()
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}
pruneCacheEntry
- 对传入的组件实例进行判断 是否存 cache 缓存对象中或者缓存标签名称不一样说明组件实例已经失效,销毁组件,直接从 cache 中移除组件的实例
function pruneCacheEntry(
cache: CacheEntryMap,
key: string,
keys: Array<string>,
current?: VNode
) {
const entry = cache[key]
if (entry && (!current || entry.tag !== current.tag)) {
entry.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
remove
export function remove(arr: Array<any>, item: any): Array<any> | void {
const len = arr.length
if (len) {
if (item === arr[len - 1]) {
arr.length = len - 1
return
}
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
cacheVNode
- 该方法添加缓存方法 判断 vnodeToCache 值是否存在组件信息 通过 keyToCache 值缓存组件的key vnodeToCache 缓存组件的消息 完成后销毁当前组件信息
- 判断 this.max && (keys.length > parseInt(this.max)) keys.length 大于 max 缓存组件数量 LRU 策略 删除第一项 (最久未使用的一项)
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)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
}
},
created
- 初始化两个变量
- cache 缓存的对象
- keys 缓存的 key 数组
created() {
this.cache = Object.create(null)
this.keys = []
},
destroyed
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
}
updated
updated() {
this.cacheVNode()
},
render
-
- 通过 this.$slots.default 获取组件的插槽内容
-
- getFirstComponentChild 获取第一个子组件的虚拟dom Vnode
-
- 判断传入的参数 max include exclude 如果不满足 return Vnode
-
- 如果组件缓存存在 根据 LRU策略移动组件在缓存数组中的位置 如果缓存不存在则 vnodeToCache = vnode 绑定其关系触发更新添加到缓存项中
-
- 完成后标记 vnode.data.keepAlive = true
render() {
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
const componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
const name = _getComponentName(componentOptions)
const { include, exclude } = this
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key =
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 {
this.vnodeToCache = vnode
this.keyToCache = key
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
为什么被缓存的组件不会执行 created 和 mounted 呢?
- createComponent 方法中 i = i.init 方法
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 )
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
- 满足 vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive 就不会执行初渲染方法 $mount
const componentVNodeHooks = {
init(vnode: VNodeWithData, hydrating: boolean): boolean | void {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
const mountedNode: any = vnode
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = (vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
))
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
- prepatch 更新了梓组件 相当于从缓存里取出了当前组件实例
prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = (vnode.componentInstance = oldVnode.componentInstance)
updateChildComponent(
child,
options.propsData,
options.listeners,
vnode,
options.children
)
},
createComponent 渲染 dom
- isReactivated 判断当前组件为缓存的组件
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 )
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
- reactivateComponent 循环执行缓存的组件生命周期
function reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
let i
let innerNode = vnode
while (innerNode.componentInstance) {
innerNode = innerNode.componentInstance._vnode
if (isDef((i = innerNode.data)) && isDef((i = i.transition))) {
for (i = 0; i < cbs.activate.length; ++i) {
cbs.activate[i](emptyNode, innerNode)
}
insertedVnodeQueue.push(innerNode)
break
}
}
insert(parentElm, vnode.elm, refElm)
}
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)
}
}
}
keep-alive 如何对包裹的组件进行缓存呢?
- 首次加载被包裹组件时,由 keep-alive.js 中的 render 函数可知,vnode.componentInstance 的值 是undefined,keepAlive 的值 是true,因为keep-alive组件作为父组件,它的render函数会先于被包裹组件执行;那么就只执行到 i(vnode, false /* hydrating */),后面的逻辑不再执行;
- 再次访问被包裹组件时,vnode.componentInstance的值就是已经缓存的组件实例,那么会执行 insert(parentElm, vnode.elm, refElm)逻辑,这样就直接把上一次的DOM插入到了父元素中。