keep-alive
这是一个非常好用的功能,主要是将组件缓存下来,这样在动态切换的时候就不需要重新实例化,可以极大的增强性能。我们也是从例子出发
<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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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)
}
2
3
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])
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
直接看第一句代码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
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
这里我们会调用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 */)
}
}
}
2
3
4
5
6
7
8
9
10
我们可以看到,其中在初始化的时候,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)
}
2
3
4
5
6
当再次点击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
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在这个方法中,因为isReactivated值为true了,再次创建组件就会走reactivateComponent。省去了initComponent的步骤,并且在该方法中 直接将vnode insert到了parentElm中。节省的性能是非常巨大的
生命周期
讲完整个流程,我们再来看看生命周期。首次渲染A组件之前就讲过了,我们看componentVNodeHooks.insert方法就能看到,执行完mounted钩子 就会执行activateChildComponent方法,该方法最后会执行callHook(vm, 'activated')。
所以输出结果是
'A mounted'
'A activated'
2
点击switch进行切换,想想就知道首先会运行A组件的卸载,之后运行B组件的首次加载。那么从流程上来说,会运行componentVNodeHooks.destroy钩子。 后面的流程就类似了。最终输出结果是
'A deactivated'
'B mounted'
'B activated'
2
3
再点一次switch这时候,keep-alive的cache中存在这两个vnode实例。它唯一的不同点在于执行insert方法的时候queueActivatedComponent方法。 它的作用在于将当前组件实例添加到了activatedChildren数组中,在微任务阶段,调用flushSchedulerQueue方法的时候处理。
最终输出为
'B deactivated'
'A activated'
2
总结
面试题
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29