前言
通过这篇文章可以了解如下内容
keep-alive
原理
<keep-alive>
在Vue 源码中是一个内置组件,它的定义在 src/core/components/keep-alive.js
中:
export default {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {},
destroyed () {},
mounted () {},
render () {}
}
Vue导出了一个对象,也就是说keep-alive
组件的内容有abstract
属性(后面说这个属性的作用)、created
、mounted
、destroyed
生命周期函数、自定义render
函数,并接收3个属性include
、exclude
、max
分别是需要缓存的组件列表,不需要缓存的组件列表和缓存列表最大长度(后面说)
接下来从下面这个demo的整个流程说起
import Vue from 'vue'
// 组件A
const A = {
name: 'A',
template: `<div class="a"> 这是 A </div>`,
created () {
console.log('A 执行了 created')
},
mounted () {
console.log('A 执行了 mounted')
},
activated () {
console.log('A 执行了 activated')
},
deactivated () {
console.log('A 执行了 deactivated')
}
}
// 组件B
const B = {
name: 'B',
template: `<div class="b"> 这是 B </div>`,
created () {
console.log('B 执行了 created')
},
mounted () {
console.log('B 执行了 mounted')
},
activated () {
console.log('B 执行了 activated')
},
deactivated () {
console.log('B 执行了 deactivated')
}
}
new Vue({
components: { A, B },
el: '#app',
template: `<div>
<keep-alive>
<component :is="com"></component>
</keep-alive>
<button @click="change">点击</button> // 点击切换组件
</div>`,
data () {
return {
com: 'A'
}
},
methods: {
change () {
this.com = this.com === 'A' ? 'B' : 'A'
}
}
})
定义了两个组件A
和B
,分别定义了 4 个生命周期。根组件引用这俩组件,并包在<keep-alive>
组件中,接下来看整个流程
初次渲染
首先创建根组件实例和根组件的Render Watcher
,然后执行根组件的render
函数创建VNode,当遇到keep-alive
组件时,会给keep-alive
创建一个组件VNode;并创建组件A
的组件VNode(插槽原理一篇中说过这种方式),将组件A
的组件VNode添加到keep-alive
的组件VNode的componentOptions.children
中。接下来执行根组件的 patch 过程并为keep-alive
组件创建Vue实例。在创建实例过程中,会调用initLifecycle
方法,方法内有这样的逻辑
export function initLifecycle (vm: Component) {
const options = vm.$options
// 子组件 Vue 实例的 options 才会有 parent 属性,属性值为上一个 Vue 实例
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
// ...
}
这块逻辑的主要目的是让组件实例建立父子关系。如果当前组件实例的options.abstract
不为true
则将当前组件实例添加到options.abstract
不为true
的父辈实例的$children
中
- 根组件的Vue实例
vm.$parent
为null
- 由于
keep-alive
组件定义中abstract
为true
,不会将自身的Vue实例添加到父级实例的$children
中,但是自身的Vue实例的$parent
属性依然指向父级实例。而子组件(A
)在创建Vue实例时,由于判断条件,会将自身添加到keep-alive
组件实例的父级实例的$children
中
如下图,keep-alive
的Vue实例的$parent
指向根实例,keep-alive
的Vue实例的$children
为空;组件A
的Vue实例的$parent
指向根实例;根实例的$parent
指向null
,根实例的$children
包含组件A
的Vue实例
接下来会调用keep-alive
的created
生命周期
created () {
this.cache = Object.create(null)
this.keys = []
}
keep-alive
在 created
生命周期里定义了 this.cache
和 this.keys
,用于存放缓存的组件,具体作用后面会说
keep-alive
的Vue实例创建完成之后,会执行keep-alive
组件的自定义render
函数创建渲染VNode
render () {
const slot = this.$slots.default // 获取插槽内容
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// 获取当前组件的名称,如果没有设置name属性则使用标签名
const name: ?string = 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 {
cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
首先通过this.$slots.default
获取插槽内容;调用getFirstComponentChild
函数获取第一个组件VNode(注意是组件VNode),也就是demo中组件A
的组件VNode;根据组件A
的组件VNode获取componentOptions
然后获取当前组件的名称,如果没有设置name
属性则使用标签名,接下来通过matches
方法判断当前组件的名称和 include
、exclude
的关系
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
}
通过matches
方法可以看出include
、exclude
两个属性可以是数组、逗号分隔的字符串、正则表达式
如果当前组件名称不在include
中或者在exclude
中,则返回当前组件的VNode;否则执行缓存流程
缓存流程如下
render () {
const slot = this.$slots.default // 获取插槽内容
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// ...
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 {
cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
创建一个key
变量,如果vnode.key
不为null
则key
的值就是vnode.key
,反之变量值是一个拼接字符串。然后拿着这个变量值去cache
属性中查找,如果没有则将当前VNode存入cache
中,将key
存入keys
中;如果传入的max
小于keys
的长度,调用pruneCacheEntry
方法,删除keys
数组中第一个元素对应的VNode,目的是清除缓存
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
// cached 有值,并且 不是当前正在渲染的 组件
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
pruneCacheEntry
方法会根据传入的key
获取cache
中对应的VNode,如果这个VNode不是正在渲染的VNode则调用这个VNode实例的$destroy()
方法卸载这个组件。这是一种LRU缓存策略,keys
数组的第一个元素对应的VNode可以理解为使用率最低的VNode;因为在render
函数中如果命中缓存,会将这个key
从keys
中取出并重新添加到最后
回到render
函数,缓存添加完成之后设置vnode.data.keepAlive = true
,然后返回VNode;这个VNode就是demo中A
组件的组件VNode。接着进入keep-alive
的patch过程,在patch过程中会创建A
组件的Vue实例并将实例挂载到vnode.componentInstance
中,然后创建A
组件的渲染VNode,当整个DOM树创建完成并插入到页面中后,会执行在整个 patch 过程收集的insert
钩子函数,其中就包含组件A
的insert
钩子函数
insert (vnode: MountedComponentVNode) {
// vnode.context 指向创建该 组件VNode 时所在的 Vue 实例
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
}
insert
方法会先调用A
组件的mounted
生命周期,然后因为A
组件的vnode.data.keepAlive
为true
但是此时父组件实例(在这里是根实例,因为组件A
的组件VNode是在根实例中创建的)的mounted
生命周期还没触发,所以不会调用queueActivatedComponent
方法,而是调用activateChildComponent(componentInstance, true)
方法
export function activateChildComponent (vm: Component, direct?: boolean) {
if (direct) {
vm._directInactive = false
if (isInInactiveTree(vm)) {
return
}
} else if (vm._directInactive) {
return
}
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
callHook(vm, 'activated')
}
}
activateChildComponent
方法逻辑如下
- 如果
direct
为true
,设置vm._directInactive = false
;调用isInInactiveTree
方法,判断传入组件实例的所有祖辈实例的_inactive
属性是不是为true
- 如果有一个为
true
,说明这个祖辈实例也是包在keep-alive
内并且不是活跃状态直接return
。也就是说如果当前实例的祖辈实例不是活跃状态,则不会调用当前实例的activated
生命周期。 - 反之如果所有祖辈实例的
_inactive
属性为false
或者没有此属性,则给组件A
的实例添加_inactive
属性,设置为false
表示此组件是活跃状态。接着遍历A
组件实例的所有子孙实例,如果子孙实例中有keep-alive
则触发对应子孙组件的activated
生命周期,然后触发自身的activated
生命周期,所以activated
执行顺序是先子后父
- 如果有一个为
- 如果
direct
为false
并且vm._directInactive
为true
,直接返回
首次渲染 B 组件
当点击demo中的按钮调用change
方法
change () {
this.com = this.com === 'A' ? 'B' : 'A'
}
先看下整体更新流程,修改com
属性,通过nextTick
将根组件的Watcher更新放到下个队列中。在创建根组件的渲染VNode过程中,会创建组件B的组件VNode。接着进入根组件的 patch 过程,在比对到keep-alive
时,由于keep-alive
是一个组件所以会调用keep-alive
的prepatch
钩子函数,从而执行updateChildComponent
方法,并调用keep-alive
的$forceUpdate
方法更新。具体流程在Vue源码(十)插槽原理中。调用keep-alive
的watcher.run
方法,执行render
函数,返回组件B
的组件VNode;接着进入keep-alive
的 patch 过程,patch过程中的oldVnode
是组件A
的组件VNode,而vnode
是组件B
的组件VNode。由于这两个组件VNode不相同,所以会创建组件B
的Vue实例并执行组件B
的挂载过程
当B
组件的渲染VNode挂载完成之后,会回到keep-alive
的patch
方法,在patch
方法中有这样一段逻辑
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
此时组件A
和组件B
都在页面上,所以会调用A
组件的destroy
钩子函数
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
因为组件A的vnode.data.keepAlive
为true
,所以会调用deactivateChildComponent
方法,而不是卸载A
组件
export function deactivateChildComponent (vm: Component, direct?: boolean) {
if (direct) {
vm._directInactive = true
if (isInInactiveTree(vm)) {
return
}
}
if (!vm._inactive) {
vm._inactive = true
// 自身变为不活跃时,触发所有 子vm 中 keep-alive 组件的 deactivated 钩子
for (let i = 0; i < vm.$children.length; i++) {
deactivateChildComponent(vm.$children[i])
}
callHook(vm, 'deactivated')
}
}
将_directInactive
属性设置为true
,并遍历祖辈实例的_inactive
是否为true
,即是否为失活状态,如果是则直接返回;否则如果组件实例的_inactive
属性为false
,说明是活跃状态,将组件实例的_inactive
设置为true
;并遍历子孙组件,如果子孙组件中有keep-alive
,并且当前状态是活跃状态,则调用子孙组件的deactivated
生命周期,然后调用自身的deactivated
生命周期。
当整个DOM树创建挂载完成之后,会调用组件B
的insert
方法
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
因为父组件(这里也是根组件,因为组件B
的组件VNode也是在根组件中创建的)已经挂载过(在首次创建过程中执行过mounted
生命周期),所以执行的是queueActivatedComponent
方法
export function queueActivatedComponent (vm: Component) {
vm._inactive = false
activatedChildren.push(vm)
}
queueActivatedComponent
方法将实例的_inactive
设置成false
表示活跃组件,并将组件添加到activatedChildren
中。也就是说如果子孙组件中也包含keep-alive
,并且当前子孙组件的mounted
执行过的话,在调用被包裹组件的insert
钩子函数时,都不是直接触发activated
生命周期,而是将这个被包裹组件的Vue实例添加到activatedChildren
中,后面会说为什么要这样做。
渲染Watcher的更新是通过flushSchedulerQueue
方法触发的,flushSchedulerQueue
方法内会循环队列中的所有Watcher并调用watcher.run
方法,所以渲染完成后回到此方法继续执行
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn()
break
}
}
}
// 从这里开始
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
// 重置所有状态
resetSchedulerState()
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
}
调用callActivatedHooks
方法,并将activatedQueue
传入,activatedQueue
内保存的是所有子孙keep-alive
包裹的组件实例
function callActivatedHooks (queue) {
for (let i = 0; i < queue.length; i++) {
queue[i]._inactive = true
activateChildComponent(queue[i], true /* true */)
}
}
遍历所有实例,并将_inactive
置为true
,然后调用activateChildComponent
去调用所有实例的activated
生命周期
这里有个疑问就是为什么要放在最后统一执行,而不是和mounted
一样在insert
钩子函数中执行?
这是因为在activateChildComponent
方法中有一个判断isInInactiveTree(vm)
,如果不提前将父辈中被keep-alive
组件包裹的组件实例的_inactive
属性置为false
的话,会直接返回而不是执行activated
生命周期函数。
缓存组件
当再次点击按钮触发更新,创建keep-alive
的组件VNode,并创建A
组件的组件VNode,VNode创建完成之后,进入根组件的 patch
阶段。当遇到keep-alive
的组件VNode时,调用keep-alive
的组件VNode的 prepatch
钩子函数,由于keep-alive
的组件VNode中有插槽节点,调用$forceUpdate
更新keep-laive
组件。执行keep-alive
的render
函数
此时A
组件的组件VNode已经缓存了,从缓存的组件VNode获取Vue实例,将实例挂载到组件A
的组件VNode上;然后更新keys
,组件A
对应的key
重新添加到keys
最后
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 { include, exclude } = this
if (
(include && (!name || !matches(include, name))) ||
(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)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
返回组件A
的组件Vnode之后,进入keep-alive
的 patch 过程,此时的oldVnode
是组件B
的组件VNode,newVnode
是组件A
的组件VNode。由于新老VNode不同,所以会调用createElm
函数,createElm
函数内当遇到组件Vnode时,会调用createComponent
方法
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// 判断组件实例是否已经存在, 并且组件被 keep-alive 包裹
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 执行 组件的 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
}
}
}
会调用A
组件的init
钩子函数
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
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)
}
},
此时,if
为true
,所以会调用A
组件的prepatch
钩子函数,而不是用createComponentInstanceForVnode
重新创建Vue实例,所以不会调用created
生命周期,并且不会再次执行挂载过程
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props 传入子组件的最新的 props 值
options.listeners, // updated listeners 自定义事件
vnode, // new parent vnode
options.children // 最新的插槽VNode
)
},
prepatch
内调用updateChildComponent
方法,在当前例子中,组件A没有更改所以不会触发组件A
的更新。回到createComponent
继续执行,触发initComponent
方法更新组件A
的组件VNode的DOM结构;并收集组件A
的 insert
钩子函数。通过insert
方法,将DOM插入到目标位置;此时页面上组件A和组件B都存在。
// createComponent 内部
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
// initComponent
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
// 将 渲染vnode 的 $el 属性赋值给 组件vnode 的 elm 属性
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
registerRef(vnode)
insertedVnodeQueue.push(vnode)
}
}
由于isReactivated
为true
,还会执行reactivateComponent
方法,方法内部解决对重新活跃组件 transition
动画不触发的问题,然后再执行一次insert
方法,这里有点奇怪感觉,不知道为啥还要执行一次insert
function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i
// hack for #4339: a reactivated component with inner transition
// does not trigger because the inner node's created hooks are not called
// again. It's not ideal to involve module-specific logic in here but
// there doesn't seem to be a better way to do it.
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
}
}
// unlike a newly created component,
// a reactivated keep-alive component doesn't insert itself
insert(parentElm, vnode.elm, refElm)
}
插入完成之后,剩下逻辑就和上面第二次点击按钮一样了。就是在keep-alive
的patch
方法最后,会将组件B
删除并调用deactivated
生命周期函数。然后调用组件A
的insert
钩子函数,由于组件A
已经挂载过所以不会执行mounted
生命周期;而是将Vue实例添加到activatedChildren
中,并将_inactive
属性置为false
。当所有组件都渲染完成之后,回到flushSchedulerQueue
中,调用callActivatedHooks
方法触发activatedChildren
中所有元素的activated
生命周期函数。
总结
keep-alive
原理
keep-alive
使用插槽的方式;在创建父组件VNode时,对被包裹组件创建组件VNode。当调用keep-alive
的render
函数时,获取默认的插槽VNode,然后判断这个组件VNode有没有缓存过:
- 如果缓存过,则给这个组件VNode设置缓存的Vue实例,防止再次创建;也就是说
keep-alive
缓存的是组件的Vue实例,每次命中缓存后,都不需要再次创建Vue实例,而是用之前缓存的实例。最后返回这个组件VNode。 - 如果没有缓存过,使用LRU缓存策略添加缓存。最后返回这个组件VNode。