本篇文章,我们来讲一下keep-alive
的实现。 更容易看懂
Vue
中,有三个内置的抽象组件,分别是keep-alive
、transition
和transition-group
,
它们都有一个共同的特点,就是自身不会渲染一个DOM元素,也不会出现在父组件链中。
keep-alive
的作用,是包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。具体的用法见这里。
该组件的定义,是在src/core/components/keep-alive.js
文件中。
它会在Vue
初始化时,添加在Vue.options.components
上,所以在所有的组件中,都可以直接只用它。
直接看代码:
export default {
name: 'keep-alive', abstract: true, props: { ... }, created () { this.cache = Object.create(null) }, destroyed () { ... }, watch: { ... }, render () { ... } }
name
不用多说,abstract: true
这个条件我们自己定义组件时通常不会用,
它是用来标识当前的组件是一个抽象组件,它自身不会渲染一个真实的DOM元素。
比如在创建两个vm
实例之间的父子关系时,会跳过抽象组件的实例:
let parent = options.parent
if (parent && !options.abstract) { while (parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) }
props
表示我们可以传入include
来匹配哪些组件可以缓存,exclude
来匹配哪些组件不缓存。
created
钩子函数调用时,会创建一个this.cache
对象用于缓存它的子组件。
destroyed
表示keep-alive
被销毁时,会同时销毁它缓存的组件,并调用deactivated
钩子函数。
function pruneCacheEntry (vnode: ?VNode) {
if (vnode) { if (!vnode.componentInstance._inactive) { callHook(vnode.componentInstance, 'deactivated') } vnode.componentInstance.$destroy() } }
watch
是在我们改变props
传入的值时,同时对this.cache
缓存中的数据进行处理。
function pruneCache (cache: VNodeCache, filter: Function) { for (const key in cache) { const cachedNode: ?VNode = cache[key] if (cachedNode) { const name: ?string = getComponentName(cachedNode.componentOptions) if (name && !filter(name)) { pruneCacheEntry(cachedNode) cache[key] = null } } } }
抽象组件没有实际的DOM元素,所以也就没有template
模板,它会有一个render
函数,我们就来看看里面进行了哪些操作。
render () {
const vnode: VNode = getFirstComponentChild(this.$slots.default) const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions if (componentOptions) { // check pattern const name: ?string = getComponentName(componentOptions) if (name && ( (this.include && !matches(this.include, name)) || (this.exclude && matches(this.exclude, name)) )) { return vnode } 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 (this.cache[key]) { vnode.componentInstance = this.cache[key].componentInstance } else { this.cache[key] = vnode } vnode.data.keepAlive = true } return vnode }
首先,调用getFirstComponentChild
方法,来获取this.$slots.default
中的第一个元素。
export function getFirstComponentChild (children: ?Array<VNode>): ?VNode { return children && children.filter((c: VNode) => c && c.componentOptions)[0] }
this.$slots.default
中包含的是什么内容,我们在《slot和作用域插槽》中已经详细的做了讲解。
从上面的方法我们可以看到,在我们会过滤掉非自定义的标签,然后获取第一个自定义标签所对应的vnode
。
所以,如果keep-alive
里面包裹的是html
标签,是不会渲染的。
然后获取componentOptions
,
vdom——VNode中我们介绍过componentOptions
包含五个元素
{ Ctor, propsData, listeners, tag, children }
。
function getComponentName (opts: ?VNodeComponentOptions): ?string { return opts && (opts.Ctor.options.name || opts.tag) }
通过getComponentName
方法来获取组件名,然后判断该组件是否合法,
如果include
不匹配或exclude
匹配,则说明该组件不需要缓存,
此时直接返回该vnode
。
否则,vnode.key
不存在则生成一个,存在则就用vnode.key
作为key
。
然后把该vnode
添加到this.cache
中,并设置vnode.data.keepAlive = true
。
最终返回该vnode
。
以上只是render
函数执行的过程,keep-alive
本身也是一个组件,
在render
函数调用生成vnode
后,同样会走__patch__
。在创建和diff
的过程中,
也会调用init
、prepatch
、insert
和destroy
钩子函数。
不过,每个钩子函数中所做的处理,和普通组件有所不同。
init (
vnode: VNodeWithData,
hydrating: boolean,
parentElm: ?Node, refElm: ?Node ): ?boolean {
if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance, parentElm, refElm ) child.$mount(hydrating ? vnode.elm : undefined, hydrating)
} else if (vnode.data.keepAlive) { // kept-alive components, treat as a patch const mountedNode: any = vnode // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode) } },
在keep-alive
组件内调用__patch__
时,如果render
返回的vnode
是第一次使用,
则走正常的创建流程,如果之前创建过且添加了vnode.data.keepAlive
,
则直接调用prepatch
方法,且传入的新旧vnode
相同。
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
函数做了哪些工作,之前也详细的介绍过,这里就不多说了。
简单的总结,就是依据新vnode
中的数据,更新组件内容。
insert (vnode: MountedComponentVNode) {
if (!vnode.componentInstance._isMounted) { vnode.componentInstance._isMounted = true callHook(vnode.componentInstance, 'mounted') } if (vnode.data.keepAlive) { activateChildComponent(vnode.componentInstance, true /* direct */) } },
在组件插入到页面后,如果是vnode.data.keepAlive
则会调用activateChildComponent
,
这里面主要是调用子组件的activated
钩子函数,并设置vm._inactive
的标识状态。
destroy (vnode: MountedComponentVNode) {
if (!vnode.componentInstance._isDestroyed) { if (!vnode.data.keepAlive) { vnode.componentInstance.$destroy() } else { deactivateChildComponent(vnode.componentInstance, true /* direct */) } } }
在组件销毁时,如果是vnode.data.keepAlive
返回true
,
则只调用deactivateChildComponent
,这里面主要是调用子组件的deactivated
钩子函数,
并设置vm._directInactive
的标识状态。因为vnode.data.keepAlive
为true
的组件,
是会被keep-alive
缓存起来的,所以不会直接调用它的$destroy()
方法,
上面我们也提到了,当keep-alive
组件被销毁时,会触发它缓存中所有组件的$destroy()
。
因为keep-alive
包裹的组件状态变化,还会触发其子组件的activated
或deactivated
钩子函数,
activateChildComponent
和deactivateChildComponent
也会做一些这方面的处理,细节大家可以自行查看。