vue 2.5+内置组件<keep-alive>使用及源码解读

212 阅读3分钟

在之前开发的混合App中H5使用vue+vant,针对移动端返回键对其中个别页面做了缓存处理,就用到了vue的内置组件 keep-alive ,今天总结一下该组件的使用及源码解析。

1.项目中的使用

话不多说直接上代码;

  • App.js中代码
    <template>
       <div id="app">
           <keep-alive>
               <router-view v-if="$route.meta.keepAlive" :key="$route.fullPath"></router-view>
           </keep-alive>
           <router-view v-if="!$route.meta.keepAlive" :key="$route.fullPath"></router-view>
       </div>
    </template>
    
    在App.js中使用keep-alive,配合router-view,将需要缓存的路由组件渲染在kepp-alive中,这里就需要配置路由时,设置路由对象的meta属性。
  • router>index.js中代码 如下属性对象meta中keepAlive属性为true既表示可被缓存
    {
        path: '/refundOrderInfo',
        name: refundOrderInfo,
        component: refundOrderInfo,
        meta: {
            keepAlive: true
        }
    },
    {
        path: '/propertyNotice',
        name: 'propertyNotice',
        component: propertyNotice
    }
    
  • 其他方式 在具体业务场景中还可以通过路由守卫函数动态设置meta.keepAlive的值,例如:
    beforeRouteLeave (to, from, next) {
        if (xxx) {
            to.meta.keepAlive = true
        }
        next()
    }
    

2.源码解析

  • keep-alive为内置组件,源码文件目录为src/core/components/keep-alive.js,接下来我会以源码内注释的形式解析,有描述错误的地方还望指正
    先看export default的内容
    export default {
        name: 'keep-alive',
        abstract: true, // 表示此组件为抽象组件,抽象组件没有真实的节点,在组件渲染的时候不会解析渲染成真实的dom节点,而只是作为中间的数据过度层处理
    
        props: {
            // include 表示只有匹配的组件会被缓存,而 exclude 表示任何匹配的组件都不会被缓存,props 还定义了 max,原以为它表示缓存的大小,再后面发现this.max会与this.keys.length作比较,大胆猜测max表示的是可允许被缓存组件个数的最大值
            include: patternTypes, // patternTypes: Array<Function> = [String, RegExp, Array],支持字符串,正则表达式,数组
            exclude: patternTypes,
            max: [String, Number]
        },
    
        created () {
            this.cache = Object.create(null) // 以键值对形式建立缓存数据对象,key为组件唯一标识,value为组件对应的vnode,
            this.keys = [] // 缓存数据的keys集合
        },
    
        // 抽象组件销毁时触发
        destroyed () {
            for (const key in this.cache) {
                pruneCacheEntry(this.cache, key, this.keys)
    
                /* 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)
                } */
                // 由于此处未传第4个参数,已缓存的数据将会全部重置为null,并将key值从keys集合中移除
            }
        },
    
        mounted () {
            // 监听include和exclude,及时对需要缓存的组件集合动态计算
            // match方法针对不同数据类型的的include,exclude做匹配判断
            this.$watch('include', val => {
            pruneCache(this, name => matches(val, name))
            })
            this.$watch('exclude', val => {
            pruneCache(this, name => !matches(val, name))
            })
            /*
                function pruneCache (keepAliveInstance: any, filter: Function) {
                    // keepAliveInstance既当前组件实例,_vnode是当前组件对应的虚拟dom树结构
                    const { cache, keys, _vnode } = keepAliveInstance
                    for (const key in cache) {
                        const cachedNode: ?VNode = cache[key]
                        if (cachedNode) {
                            const name: ?string = getComponentName(cachedNode.componentOptions) // 获取子组件name,如果子组件有name属性则取name值,如果没有则取使用时的标签名
                            if (name && !filter(name)) {
                                // 如果未匹配中,将会重新遍历cache,删除不该再被缓存的组件数据
                                pruneCacheEntry(cache, key, keys, _vnode)
                            }
                        }
                    }
                }
            */
        },
        ...
        最后keep-alive是直接使用render函数
        render () {
            const slot = this.$slots.default // 取默认插槽 
            const vnode: VNode = getFirstComponentChild(slot) // 获取默认的第一个子节点
            // <keep-alive> 只处理第一个子元素,所以一般和它搭配使用的有 component 动态组件或者是 router-view
            const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
            if (componentOptions) {
                // check pattern
                const name: ?string = getComponentName(componentOptions)
                const { include, exclude } = this
    
                // 组件名如果满足了配置 include 且不匹配或者是配置了 exclude 且匹配,那么就直接返回这个组件的 vnode
                if (
                    // 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]) {
                    // 如果cache[key]存在, 直接从缓存中拿vnode的组件实例
                    vnode.componentInstance = cache[key].componentInstance
                    // make current key freshest
                    // 由于key可能是在上述重新定义了,所以需要更新keys集合中key值
                    remove(keys, key)
                    keys.push(key)
                } else {
                    若cache[key]不存在,设置缓存,将当前key放入keys集合中
                    cache[key] = vnode
                    keys.push(key)
                    // prune oldest entry
                    // 最后的逻辑,判断缓存的个数是否超过max设置值,如果超出,将oldest key移除,由此可见keys集合是按先进先出队列结构维护的
                    if (this.max && keys.length > parseInt(this.max)) {
                    pruneCacheEntry(cache, keys[0], keys, this._vnode)
                }
            }
            // 此处为何要这样设置,待我了解了,后续更新
            vnode.data.keepAlive = true
            return vnode || (slot && slot[0])
        }