keep-alive原理
什么是keep-alive
<keep-alive>
是vue
实现的一个内置组件,是一个抽象组件,它自身不会渲染一个dom
元素,也不会出现在父组件链中。当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
keep-alive的作用
所以我们可以把一些不常变动的组件或者需要缓存的组件用<keep-alive>
包裹起来,这样keep-alive
就会帮我们把这些组件保存在内存中,而不是直接销毁,这样就可以保留组件的状态并且避免多次重新渲染。
属性
<keep-alive>
组件接收三个属性:
include
- 字符串或正则表达式。只有名称匹配的组件会被缓存exclude
- 字符串或正则表达式。任何名称匹配的组件都不会被缓存max
- 数字。最多可以缓存多少组件实例
keep-alive的源码分析
<keep-alive>
组件的定义位于源码src/core/components/keep-alive.js
文件中,代码如下:
const patternTypes: Array<Function> = [String, RegExp, Array]
export default {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
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)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
}
},
created () {
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
this.cacheVNode()
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
updated () {
this.cacheVNode()
},
// 核心方法
render () {
// 获取默认插槽中的第一个组件节点
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
// 获取该组件节点的componentOptions
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
// 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
// 如果name不在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
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
// delay setting the cache until update
this.vnodeToCache = vnode
this.keyToCache = key
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
该内置组件并没有<template></template>
标签,但其内部有一个render
函数。所以<keep-alive>
是一个函数式组件。执行<keep-alive>
组件渲染时会执行其render
函数。
created
在create
钩子函数中定义并初始化了两个属性:this.cache
和this.keys
created () {
this.cache = Object.create(null)
this.keys = []
},
this.catch
是一个对象用来存储需要缓存的组件,是以以下形式存储
this.catch = {
'key1' : '组件1',
'key2' : '组件2',
// ...
}
this.keys
是一个数组,用来存储每个需要缓存的组件的key
,即对应的this.cache
对象中的键值
destoryed
当<keep-alive>
组件被销毁时,会调用这个钩子函数。在这个函数中会遍历this.catch
对象,然后将被缓存的并且当时没有处于渲染状态的组件都销毁并将其从this.catch
对象中删除。
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
}
// pruneCacheEntry函数,在同一个文件中
function pruneCacheEntry (
cache: CacheEntryMap,
key: string,
keys: Array<string>,
current?: VNode
) {
const entry: ?CacheEntry = cache[key]
// 当前组件是否处于渲染状态,如果当前不是在被渲染中,则销毁该组件
if (entry && (!current || entry.tag !== current.tag)) {
entry.componentInstance.$destroy()
}
// 并将其从catch与keys中删除
cache[key] = null
remove(keys, key)
}
mounted
mounted
钩子函数中主要观测include
和``exclude`变化。
mounted () {
this.cacheVNode()
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}
// pruneCache函数
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)
}
}
}
}
// pruneCacheEntry函数
function pruneCacheEntry (
cache: CacheEntryMap,
key: string,
keys: Array<string>,
current?: VNode
) {
const entry: ?CacheEntry = cache[key]
// 判断this.catch中的每一个缓存组件是否与心的规则匹配,如果不匹配则销毁该组件
if (entry && (!current || entry.tag !== current.tag)) {
entry.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
在该函数内对this.cache
对象进行遍历,取出每一项的name
值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry
函数将这个已经不需要缓存的组件实例先销毁掉,然后再将其从this.cache
对象中剔除。
render
render
函数是<keep-alive>
中的核心函数,真正实现缓存功能的逻辑都在该函数中。
// 核心方法
render () {
// 获取默认插槽中的第一个组件节点 因为我们也是在<keep-alive>标签内部写dom,所以可以先获取它的默认插槽,再获取它的第一个子节点
const slot = this.$slots.default
// 获取默认插槽内的第一个组件节点
const vnode: VNode = getFirstComponentChild(slot)
// 获取该组件节点的componentOptions
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
// 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
// 如果name不在include中或者存在于exclude中则表示不缓存,则直接返回vnode
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
// 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
// 如果命中缓存,则直接从缓存中拿vnode的组件实例
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
// 调整该组件key的顺序,将其从原来的地方删掉并重新放在最后
remove(keys, key)
keys.push(key)
} else {
// delay setting the cache until update
// 如果没有命中缓存,设置对应变量,当触发update钩子
this.vnodeToCache = vnode
this.keyToCache = key
}
// 渲染和执行被包裹组件的钩子函数需要用到
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
updated () {
this.cacheVNode()
}
methods: {
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this
// this.keyToCache是否存在,如果存在进行判断
if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache
// 将该dom对象推入缓存
cache[keyToCache] = {
name: getComponentName(componentOptions),
tag,
componentInstance,
}
keys.push(keyToCache)
// prune oldest entry
// 如果长度超出限制,删除第一个
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
}
},
步骤总结
- 获取
keep-alive
对象包括的第一个子组件对象 - 根据白黑名单是否匹配返回本身的
vnode
- 根据
vnode
的cid
和tag
生成的key
,在缓存对象中判断是否有当前vnode
的缓存,如果有则返回,并更新key
在keys
中的位置 - 如果当前缓存对象不存在缓存中,就往
cache
添加这个内容,并且根据LRU
算法删除最近没有使用的实例 - 设置为第一个子组件对象的
keep-alive
为true