keep-alive源码分析
[[toc]]
如何使用
想看具体用法看我上一篇文章 这大概是最全乎的keep-alive的踩坑指南
源码剖析
源码地址
import {isRegExp, remove} from 'shared/util' import {getFirstComponentChild} from 'core/vdom/helpers/index'
function getComponentName(opts) { return opts && (opts.Ctor.options.name || opts.tag) }
function matches(pattern, name) { 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 }
function pruneCache(keepAliveInstance, filter) { const {cache, keys, _vnode} = keepAliveInstance for (const key in cache) { const cachedNode = cache[key] if (cachedNode) { const name = getComponentName(cachedNode.componentOptions) if (name && !filter(name)) { pruneCacheEntry(cache, key, keys, _vnode) } } } }
function pruneCacheEntry(cache, key, keys, current) { const cached = cache[key] if (cached && (!current || cached.tag !== current.tag)) { cached.componentInstance.$destroy() } cache[key] = null remove(keys, key) }
const patternTypes = [String, RegExp, Array]
export default { name: 'keep-alive', abstract: true, props: { include: patternTypes, exclude: patternTypes, max: [String, Number] }, created() { this.cache = Object.create(null) this.keys = [] }, destroyed() { for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys) } }, mounted() {
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 = getFirstComponentChild(slot) const componentOptions = vnode && vnode.componentOptions if (componentOptions) { const name = 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]) } }
|
其实大致可以分为这几步:
- 在要缓存的组件上使用keep-alive标签
- 根据传递的参数,看是否要添加缓存和限制的个数,不缓存直接返回你当前的vnode,若需要缓存就根据生成的key进行对象存储
- 存储的过程要注意 max 和存储的位置,如果大于max就要把索引是1的key删除, 实现置换位置。
- 将该组件实例的keepAlive属性值设置为true(this.$vnode.data.keepAlive 可以获取到,多的两个声明周期都是通过这个判断)
钩子函数
只执行一次的钩子
keep-alive
是使用你之前存储的vnode,然后直接转换成真实dom,是发生在diff之后 patch阶段,所以缓存的组件是没有 created,mounted
这些生命周期的,具体看下面的代码分析。
const componentVNodeHooks = { 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) } },
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) { const options = vnode.componentOptions const child = vnode.componentInstance = oldVnode.componentInstance updateChildComponent( child, options.propsData, options.listeners, vnode, options.children ) }, insert (vnode) { 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) } } }, destroy (vnode) { const { componentInstance } = vnode if (!componentInstance._isDestroyed) { if (!vnode.data.keepAlive) { componentInstance.$destroy() } else { deactivateChildComponent(componentInstance, true) } } } }
|
可以看出,当vnode.componentInstance(第一次进来是空的)和keepAlive同时为true时,不再进入$mount过程,那mounted之前的所有钩子函数(beforeCreate、created、mounted)都不再执行。
调用activated
在patch的阶段,最后会执行invokeInsertHook函数,而这个函数就是去调用组件实例(VNode)自身的insert
钩子,就是上面的那段代码。
function invokeInsertHook (vnode, queue, initial) { if (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data.pendingInsert = queue } else { for (let i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]) } } }
|
就是上面componentVNodeHooks的insert的方法
insert (vnode) { 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) } } }
|
看下activateChildComponent函数
export function deactivateChildComponent (vm, direct) { if (!vm._inactive) { vm._inactive = true for (let i = 0; i < vm.$children.length; i++) { deactivateChildComponent(vm.$children[i]) } callHook(vm, 'deactivated') } }
|
deactivated
钩子函数也是一样的原理,在组件实例(VNode)的destroy钩子函数中调用deactivateChildComponent
函数。
渲染
keep-alive组件的渲染
/src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) { const options = vm.$options let parent = options.parent if (parent && !options.abstract) { while (parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) }
|
keep-alive包裹的组件是如何使用缓存的?
在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) } if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }
|