前言
通过这篇文章可以了解如下内容
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。