keep-alive是什么,它有什么作用
keep-alive
是vue的内置组件,无需单独安装手动注册,keep-alive
不会向DOM添加额外节点。keep-alive
是一个能提供缓存功能,保存子组件内部状态的组件。keep-alive
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。- 当被缓存的子组件再次切换为活动状态时,不会执行其完整的生命周期,而是相应的执行
activated、deactivated
生命周期函数。
keep-alive的用法
Props:
include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
max - 数字。最多可以缓存多少组件实例。
- 路由中使用:
<keep-alive :include="includeList" :exclude="excludeList" :max="maxNum">
<router-view />
</keep-alive>
- 态组件中使用:
<keep-alive>
<component :is="view"/>
</keep-alive>
源码分析
// 源码 => vue/src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true, //定义抽象组件 判断当前组件虚拟dom是否渲染成真实dom
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {
this.cache = Object.create(null) // 缓存VNode
this.keys = [] // 缓存VNode的key
},
destroyed () {
// 销毁时删除所有缓存的VNode
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
// 监听 include和exclude属性,及时的更新缓存
// pruneCache 对cache做遍历,把不符合新规则的VNode从缓存中移除
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
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 (
// not included || excluded
(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)
// 超出缓存最大数量,移除最前面的VNode
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
// 标记
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
分析
1、created
时创建cache、keys
缓存容器,用于收集需要缓存的VNode。
2、mounted
时监听include、exclude
,在缓存规则改变的时候过滤更新缓存集合cache、keys
。其中pruneCache
对cache
做遍历,它的核心是pruneCacheEntry
,pruneCacheEntry
会调用需要过滤的组件实例的$destroy()
,销毁组件实例。
function pruneCache (keepAliveInstance: any, filter: Function) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode: ?VNode = cache[key]
if (cachedNode) {
const name: ?string = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
3、destroyed
删除所有缓存的VNode,并调用对应组件实例的destory
钩子函数。
4、keep-alive
不是使用常规的templete模版的方式,而是直接实现了一个render
函数,所以每次触发渲染的时候都会执行render
函数。render
函数的作用就是获取第一个子组件,先对其进行include、exclude
判断,接下来再判断如果不存在于cache
中,则将其添加至cache
的末尾;如果已存在于cache
中,则将其调整至cache
的末尾。这样做的目的是将较为活跃的缓存组件保存在cache
的末尾,当缓存的组件数量大于设定的max
值时,销毁cache
中位置靠前(不活跃)的组件实例。
5、keep-alive
是一个抽象组件,不会生成真实的节点。由上知,keep-alive
定义了abstract:true
属性。那vue会忽略该组件的实例,因此不会在DOM树上生成相应的节点。
// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
const options= vm.$options
// 找到第一个非abstract父组件实例
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
// ...
}
keep-alive渲染过程
Vue的渲染过程:new Vue -> init -> $mount -> compile -> render -> vnode -> patch -> DOM
那被keep-alive
包裹的组件和普通的组件有哪些不同呢?我想熟悉vue的人都应该能回答的上来:保存了组件内部的状态,被缓存后不会执行created、mounted
等钩子函数,相应的会执行存activated、deactivated
两个生命周期钩子函数。
可那又是怎么实现的呢??我们带着问题接着往下看。
举一例子:
<template>
<div id="app">
<keep-alive>
<component :is='view'></component>
<p>这里是一个文本</p>
</keep-alive>
<el-button @click="changeView">切换组件</el-button>
</div>
</template>
<script>
import A from './views/A'
import B from './views/B'
export default {
name: 'App',
components: {
A,
B
},
data(){
return {
view:'A'
}
},
methods: {
changeView(){
this.view = this.view === 'A' ? 'B' : 'A'
}
},
};
</script>
首次渲染A:
这里没有渲染p标签也验证了keep-alive
只渲染第一个子组件。
Vue 的渲染最后都会到patch
过程,而组件的patch
过程会执行createComponent
方法。
// src/core/vdom/patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
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
变量,表示是否是已经被缓存的组件。由于是首次渲染,vnode.componentInstance
为 undefined
,所以isReactivated为false
。因此和普通组件的渲染的流程一样,接下来执行initComponent
方法。
// src/core/vdom/patch.js
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
接下来我们切换到组件B
由于B也是首次渲染,可以看到与普通组件几乎没有区别。多执行了activated
生命周期。
再次切换到A组件
这里在patch
过程之前会执行prepatch
的钩子函数。流程如下:
// src/core/vdom/create-component
const componentVNodeHooks = {
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
}
}
prepatch
核心逻辑就是执行updateChildComponent
方法:
// src/core/instance/lifecycle.js
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
const hasChildren = !!(
renderChildren ||
vm.$options._renderChildren ||
parentVnode.data.scopedSlots ||
vm.$scopedSlots !== emptyObject
)
// ...
if (hasChildren) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
}
updateChildComponent
方法中,我们需要关注hasChildren
部分。因为<keep-alive>
组件本质上相当于存在一个default slot
,所以hasChildren
为真,即执行updateChildComponent
时需要对slots
做重新解析。并触发<keep-alive>
组件实例$forceUpdate
逻辑,也就是重新执行<keep-alive>
的render
方法。然后命中cache
缓存,直接返回缓存中的vnode.componentInstance
,接着进入patch
过程,执行createComponent
方法。
// src/core/vdom/patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
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
,在执行initComponent
函数的时候不会再执行组件的mount
过程了,这也就是被<keep-alive>
包裹的组件在有缓存的时候就不会在执行组件的created、mounted
等钩子函数的原因了。
然后执行reactivateComponent
:
// src/core/vdom/patch.js
function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i
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)
}
reactivateComponent
通过执行insert(parentElm,vnode.elm, refElm)
就把缓存的 DOM对象直接插入到目标元素中。
以上完成了数据更新的情况下的渲染过程,那activated、deactivated
生命周期是在什么情况下执行的呢?上面说到,在渲染的最后一步,会执行invokeInsertHook(vnode,insertedVnodeQueue,isInitialPatch)
函数执行vnode
的insert
钩子函数:
// src/core/vdom/create-component.js
const componentVNodeHooks = {
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
// ...
}
这里判断如果是被<keep-alive>
包裹的组件已经mounted
,那么则执行queueActivatedComponent(componentInstance)
,否则执行activateChildComponent(componentInstance, true)
// src/core/instance/lifecycle.js
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')
}
}
可以看到这里就是执行组件的acitvated
钩子函数,并且递归去执行它的所有子组件的activated
钩子函数。
//src/core/observer/scheduler.js
export function queueActivatedComponent (vm: Component) {
vm._inactive = false
activatedChildren.push(vm)
}
这个逻辑很简单,把当前vm实例添加到activatedChildren
数组中,等所有的渲染完毕,在nextTick
后会执行flushSchedulerQueue
。
function flushSchedulerQueue () {
// ...
const activatedQueue = activatedChildren.slice()
callActivatedHooks(activatedQueue)
// ...
}
function callActivatedHooks (queue) {
for (let i = 0; i < queue.length; i++) {
queue[i]._inactive = true
activateChildComponent(queue[i], true) }
}
也就是遍历所有的activatedChildren
,执行activateChildComponent
方法。通过队列调的方式就是把整个activated
时机延后了。
有activated
钩子函数,也就有对应的deactivated
钩子函数,它是发生在vnode
的destory
钩子函数:
// src/core/vdom/create-component.js
const componentVNodeHooks = {
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}
对于<keep-alive>
包裹的组件而言,它会执行deactivateChildComponent(componentInstance, true)
方法
// src/core/instance/lifecycle.js
export function deactivateChildComponent (vm: Component, direct?: boolean) {
if (direct) {
vm._directInactive = true
if (isInInactiveTree(vm)) {
return
}
}
if (!vm._inactive) {
vm._inactive = true
for (let i = 0; i < vm.$children.length; i++) {
deactivateChildComponent(vm.$children[i])
}
callHook(vm, 'deactivated')
}
}
总结
通过以上分析,我们已经知道keep-alive
组件能够缓存子组件,也知道了它实现的原理。在需要反复创建组件的时候可以使用keep-alive
提高性能。
这个时候我又想到了v-if
和v-show
:
v-if: 渲染开销小,切换即重新渲染, 切换开销大。
v-show:一开始便渲染所有,渲染开销大。
keep-alive: 初始渲染开销小,切换时如果不存在缓存则开始渲染,如果已存在缓存则读取缓存。
可以看到keep-alive
似乎就是存在于v-if
和v-show
之间的一个综合考虑渲染开销和切换开销的特殊存在。
参考
- vue技术揭秘
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!