[Vue 源码] keep-alive 组件逻辑分析(上)
keep-alive
基本用法
keep 的使用只需要在动态组件的最外层添加标签即可
<div id="app">
<button @click="changeTabs('child1')">child1</button>
<button @click="changeTabs('child2')">child2</button>
<keep-alive>
<component :is="chooseTabs">
</component>
</keep-alive>
</div>
var child1 = {
template: '<div><button @click="add">add</button><p>{{num}}</p></div>',
data() {
return {
num: 1
}
},
methods: {
add() {
this.num++
}
},
}
var child2 = {
template: '<div>child2</div>'
}
var vm = new Vue({
el: '#app',
components: {
child1,
child2,
},
data() {
return {
chooseTabs: 'child1',
}
},
methods: {
changeTabs(tab) {
this.chooseTabs = tab;
}
}
})
当动态组件在 child1 和 child2 之间来回切换时,第二次以后在切到 child1 组件, child1 保留着原来的数据状态。
从模版编译到虚拟 DOM
内置组件和普通组件在编译过程中没有区别,不管是哪只组件还是用户定义组件,本质上组件在模版编译成 render 函数的处理方式是一致的。这里不在分析 render 函数的生成过程。
在拿到 render 函数之后,开始生成虚拟 DOM , 由于 keep-alive 是组件,所以会调用 createComponent 函数去创建子组件的虚拟 DOM , 在 createComponent 环节中,和创建普通组件不同之处在于, keep-alive 的虚拟 DOM 会去除多余的属性,除了 slot 属性之外(slot 属性也在 2.6 版本之后被废弃),其他属性都没有意义。在 keep-alive 组件的虚拟 DOM 上,存在一个 abstract 属性作为抽象组件的标志。
// 创建子组件Vnode过程
function createComponent(Ctordata,context,children,tag) {
// abstract是内置组件(抽象组件)的标志
if (isTrue(Ctor.options.abstract)) {
// 只保留slot属性,其他标签属性都被移除,在vnode对象上不再存在
var slot = data.slot;
data = {};
if (slot) {
data.slot = slot;
}
}
}
初次渲染
keep-alive
之所以特别,是因为他不会重复渲染相同的组件,只会利用初次渲染保留的缓存去更新节点,为了更全面的了解 keep-alive
的实现原理,我们冲他的首次渲染开始分析。
流程分析
和普通组件相同的是, Vue
会拿到前面生成的虚拟 DOM
对象,执行真实节点的创建过程,也就是 patch
过程,在创建节点过程中, keep-alive
的虚拟 DOM
会被认为是一个组件的 vnode
,因此会进入 createComponent
函数,在该函数中对 keep-alive
组件进行初始化和实例化。
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// isReactivated 用来判断组件是否缓存
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)) {
// 其中一个作用就是保留真实 DOM 的虚拟 DOM 中
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
keep-alive 组件会先调用内部的 init 钩子进行初始化操作,
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
// 将组件实例复制给虚拟 DOM 的 componentInstance 属性
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
// 创建组件实例之后进行挂载
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
// ...
},
insert (vnode: MountedComponentVNode) {
// ...
},
destroy (vnode: MountedComponentVNode) {
//...
}
}
在第一次执行,组件的 vnode
对象中没有 componentInstance
属性, vnode.data.keepAlive
也没有值,所以会调用 createComponentInstanceForVnode
方法进行组件实例化并将组件实例复制给 vnode 的 componentInstance
属性,最终执行组件实例的 $mount 方法进行实例挂载。
createComponentInstanceForVnode
就是组件实例化的过程,这一过程前面已经分析过了,主要包含选项合并 、 初始化事件 、 生命周期等初始化操作。
内置组件选项
在使用组件的时候经常利用对象的形式定义组件选项,包括 data
、 method
、 computed
等,并在父组件或者全局中进行注册,来看一下 keep-alive
的具体选项
export default {
name: 'keep-alive',
abstract: true,
// keep-alive 组件允许的 props 值
props: {
include: patternTypes, // 需要进行缓存的组件名称
exclude: patternTypes, // 不需要进行缓存的组件名称
max: [String, Number] // 最大缓存数量,超过该数量时, 依据 lru 算法进行替换
},
created () {
// 缓存组件 vnode
this.cache = Object.create(null)
// 缓存组件名称
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
// 对于动态的 include 和 exclude , 需要进行监听
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
// 渲染函数
render () {
// 拿到 keep-alive 下插槽的值
const slot = this.$slots.default
// 获取第一个 vnode 节点
const vnode: VNode = getFirstComponentChild(slot)
// 拿到第一个组件实例
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
// 拿到第一个子组件 vnode 的 name 属性
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// 判断子组件是否需要进行缓存,不需要缓存时直接返回 vnode 对象
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
// 命中缓存,
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
// 删除 key 之后在重新添加,最近使用到的缓存会放在数组后面,这是 lru 算法思想,后面详细分析
remove(keys, key)
keys.push(key)
} else {
// 初次渲染,缓存 vnode
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
// 为缓存组件加上标志
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}
keep-alive
组件和普通组件在选项上是类似的, keep-alive
组件使用了 render
函数而不是 template
模版。 keep-alive
组件本质上只是存缓存和取缓存的过程,并没有实际的节点渲染。
缓存 VNode
keep-alive
组件在实例化之后会进行组件的挂载,而挂载过程又回到了 vm._render
和 vm_update
的过程。 由于 keep-alive
拥有 render
函数,所以我们直接分析 render
函数的实现
获取 keep-alive 组件插槽的内容
首先是通过 getFirstComponentChild
方法获取 keep-alive
下插槽的内容,也就是 keep-alive
组件需要渲染的子组件,
export function getFirstComponentChild (children: ?Array<VNode>): ?VNode {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const c = children[i]
// 组件实例存在,则返回,理论上返回第一个组件的 vnode
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c
}
}
}
}
缓存组件
拿到子组件实例后,需要判断是否满足缓存的匹配条件,匹配条件可以使用数组、字符串、正则
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// 判断子组件是否需要进行缓存,不需要缓存时直接返回 vnode 对象
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
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)
}
/* istanbul ignore next */
return false
}
如果组件不满足缓存的要求,则会之间返回组件的 vnode
,然后进入挂载流程
render
函数关键的一步就是缓存 vnode
,由于第一次执行 render
函数,选项中的 cache
和 keys
都没有数据,无法命中缓存,因此,在第一次渲染 keep-alive
组件是,会将需要渲染的子组件 vnode
进行缓存。将已经缓存的 vnode
搭上标志,并将子组件的 vnode
进行返回, vnode.data.keepAlive = true
真实节点的保存
再回到 crateComponent
的逻辑,在 createComponent
方法中,首先会执行 keep-alive
组件的初始化流程,也包括了子组件的挂载,之后在 createComponent
方法中拿到了 keep-alive
组件的实例,接下来重要的一步就是将真实 DOM
保存在 vnode
中。
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// isReactivated 用来判断组件是否存在缓存
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)) {
// 其中一个作用就是保留真实 DOM 的虚拟 DOM 中
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
// 将真实 DOM 保存到 vnode 中
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)
}
}
在进行组件缓存是,需要将组件的真实 DOM
节点保存到 vnode
对象上,而保存大量的 DOM
元素会非常耗费性能,因此我们需要严格控制缓存组件的数量,另外在缓存策略上也需要做优化。
对 keep-alive
组件的初次渲染做一个总结:内置的 keep-alive
组件,在子组件第一次进行渲染是将 vnode
和真实 DOM
进行了缓存
转载自:https://juejin.cn/post/7204243582378377276