keep-alive
看该组件的源码,要对组件初始化和slots有一个清晰的认识,如果还不清楚可以看我之前的文章
这是一个非常好用的功能,主要是将组件缓存下来,这样在动态切换的时候就不需要重新实例化,可以极大的增强性能。从一个简单的例子出发
<div id="app">
<keep-alive>
<component :is="currentComp"></component>
</keep-alive>
<button @click="change">switch</button>
</div>
<script>
const A = {
template: '<div class="a">' + '<p>A Comp</p>' + '</div>',
name: 'A',
mounted () {
console.log('A mounted')
},
activated () {
console.log('A activated')
},
deactivated () {
console.log('A deactivated')
}
}
const B = {
template: '<div class="b">' + '<p>B Comp</p>' + '</div>',
name: 'B',
mounted () {
console.log('B mounted')
},
activated () {
console.log('B activated')
},
deactivated () {
console.log('B deactivated')
}
}
const vm = new Vue({
el: '#app',
data: {
currentComp: 'A'
},
methods: {
change () {
this.currentComp = this.currentComp === 'A' ? 'B' : 'A'
}
},
components: {
A,
B
}
})
</script>
keep-alive的注册
本质上keep-alive
也是一个组件,只是这个组件是vue
定义的。那么问题是keep-alive
是什么时候注册的?
首先我们知道一点,keep-alive
是可以直接使用的,并不需要我们去注册。那么可以肯定它是全局注册。然后从前面的文章我们知道
全局注册是通过Vue.extend
实现的,那么显然keep-alive
应该也是。
我们查看core/global-api/index.js
文件,可以看到下面这段代码
export function initGlobalAPI() {
extend(Vue.options.components, builtInComponents)
}
builtInComponents
就是components/index
的引入,这样在Vue.options.components
下就有keep-alive
组件了,那么用户就能直接使用它。
keep-alive的执行
init
keep-alive
的执行也是和一般组件一样,通过各个钩子,从之前的关于component
的文章我们知道,组件有一个钩子对象componentVNodeHooks
其中包含了四个钩子init prepatch insert destroy
这几个就是内置的组件初始化到销毁的过程。而我们就从这里开始看
从上面的例子看,第一个组件就是keep-alive
那么直接在init
方法中打上断点进入就可以,该方法主要做了两点
- 判断有无实例,没有,新建实例;有调用
prepatch
。 - 调用
$mount
在初始化过程中,会调用created
钩子函数,就会执行cache
和keys
的初始化。在初始化过程中会执行到keep-alive
组件的vm._update(vm._render(), hydrating)
。
这里我们注意一下,看keep-alive
的源码,它是定义了render
的,不存在template
所以在这里vm._render()
方法中调用的render.call()
其实调用的是keeep-alive
中的render
方法
下面我们看render
方法
render () {
// 拿到默认的子节点
const slot = this.$slots.default
// 拿到第一个组件节点
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
const name: ?string = getComponentName(componentOptions)
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 {
this.vnodeToCache = vnode
this.keyToCache = key
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
直接看第一句代码this.$slots.default
这里拿到的是子组件的vnode
,它是从哪里来的?这块其实和之前分析的slots
有关系,没看的可以去看看,里面有详细分析。
这里slots
拿到的是数组,通过getFirstComponentChild
方法拿到第一个vnode
节点。然后将vnode
放到了vnodeToCache
中,并且将key
放到了keyToCache
数组中。
并且将data.keepAlive
置为true
。
insert
init
之后,我们已经能看到A
组件被渲染出来了。但是keep-alive
的处理还没完成,我们看insert
方法,在其中有一行代码callHook(componentInstance, 'mounted')
,也就是说
当我们执行到vnode
是keep-alive
的时候,会执行mounted
钩子,我们看这个钩子内的方法
mounted () {
this.cacheVNode()
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
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
}
}
这里我们会调用cacheVnode
方法,和方法名一样,这里主要处理了两步
- 将
vnode
实例放到了cache
中,将key
放到了keys
数组中 - 如果定义了
max
则对cache
进行处理,这里做了一个算法处理,LRU
,最近最少使用原则。它是一种缓存淘汰算法
最后对include
和exclude
进行了监听,这样首次渲染就执行完了。
prepatch
就上面的例子,我们点击一下switch
,这时候从之前可以知道,会触发patchVnode
,在该方法中就会执行prepatch
方法。
prepatch
中主要就是执行了updateChildComponent
方法,在其中执行了组件的切换,其中会再次执行到keep-alive
中的render
方法,
对B
组件进行缓存。
destroy
因为执行了切换,所以在patch
中会执行到removeVnodes
方法,这个方法里就会调用destroy
钩子,在该钩子中
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
我们可以看到,其中在初始化的时候,data
中的keep-alive
是true
,所以这里不是直接调用$destroy
而是deactivateChildComponent
方法,
很明显该方法调用了deactivated
钩子。
这里我们要注意一点,B
组件是更新过来的,所以它会走updateHook
,所以它会调用keep-alive
中的update
方法,进行B
组件的cacheVNode
。
这样两个组件都放到cache
中了。
多次切换组件缓存使用
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
}
当再次点击switch
,这时候两个组件都被缓存到cache
里了,在执行render
方法的时候,我们可以方便的拿到缓存,那么之后的操作就不用再次初始化了。除了不用初始化
去澳门看看patch
中的createComponent
方法
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
}
}
}
在这个方法中,因为isReactivated
值为true
了,再次创建组件就会走reactivateComponent
。省去了initComponent
的步骤,并且在该方法中
直接将vnode
insert
到了parentElm
中。节省的性能是非常巨大的
生命周期
讲完整个流程,我们再来看看生命周期。首次渲染A
组件之前就讲过了,我们看componentVNodeHooks.insert
方法就能看到,执行完mounted
钩子
就会执行activateChildComponent
方法,该方法最后会执行callHook(vm, 'activated')
。
所以输出结果是
'A mounted'
'A activated'
点击switch
进行切换,想想就知道首先会运行A
组件的卸载,之后运行B
组件的首次加载。那么从流程上来说,会运行componentVNodeHooks.destroy
钩子。
后面的流程就类似了。最终输出结果是
'A deactivated'
'B mounted'
'B activated'
再点一次switch
这时候,keep-alive
的cache
中存在这两个vnode
实例。它唯一的不同点在于执行insert
方法的时候queueActivatedComponent
方法。
它的作用在于将当前组件实例添加到了activatedChildren
数组中,在微任务阶段,调用flushSchedulerQueue
方法的时候处理。
最终输出为
'B deactivated'
'A activated'
总结
面试题
keep-alive
原理是什么?
这是vue
的内置组件,它通过this.$slots
获取所有的子组件vnode
,并将其缓存到cache
中,通过监听include
和exclude
去判断组件是否需要缓存。当用户触发组件切换的时候就会去缓存拿实例,而不是重新创建。
这极大的减少了组员的浪费。
除了之前说的两个参数,还有一个max
参数,当用户定义了max
参数以后,就会在缓存处理上进行改变,会使用LRU
算法。
一般来说讲到这里,有可能会让你写或者说明该算法的特性和伪代码。
- 每次都将最新获取的值的
key
放到keys
数组的最后,这样就保证最少使用的在keys
数组的最前面 - 当存放的
keys
数组不够大的时候,删除第一个数据
下面是数组的实现
function LRUCache (capacity) {
this.capacity = capacity
this.keys = new Set()
this.cache = Object.create(null)
}
LRUCache.prototype.get = function (key) {
if (this.keys.has(key)) {
this.keys.delete(key)
this.keys.add(key)
return this.cache[key]
}
return -1
}
LRUCache.prototype.put = function (key, value) {
if (this.keys.has(key)) {
this.keys.delete(key)
this.cache[key] = value
this.keys.add(key)
} else {
this.keys.add(key)
this.cache[key] = value
if (this.capacity && this.keys.size > this.capacity) {
const deleteKey = Array.from(this.keys)[0]
delete this.cache[deleteKey]
this.keys.delete(deleteKey)
}
}
return null
}