• 前言
    • 简介

      keep-alive是vue内置的抽象组件(组件自身不会渲染成真实的dom元素),其主要的作用是将需要缓存的组件保存在内存中,下次需要用到该组件时直接从缓存中获取,从而减少了重新渲染组件的开销。

    • 使用场景举例

      从菜单下的列表页(userPage)跳转到新增页(addUserPage),要求从addUserPage回到到userPage时记住用户之前的列表筛选条件,同时请求最新的列表数据。
      使用keep-alive即可轻松实现,而无需自己编程来实现保存列表筛选条件的处理逻辑。

      //  /src/views/menu3/index.vue
      <template>
          <keep-alive include="UserPage">
              <router-view />
          </keep-alive>
      </template>
      
      // /src/router/index.js  配置上述页面路由关系
      {
            path: "/user",
            component: (resolve) => { return require(["@/views/menu3"], resolve); },
            //嵌套子路由
            children: [
              {
                path: "/user/page",
                name: "UserPage",
                component: (resolve) => { return require(["@/views/menu3/page1/index.vue"], resolve); },
              }, {
                path: "/user/add",
                component: (resolve) => { return require(["@/views/menu3/page1/add.vue"], resolve); },
              }
            ]
          },
          
      // 列表页(UserPage)
        export default{
          data(){
            return{};
          },
          created(){},
          activated(){
           // 发送ajax请求获取最新列表数据
          },
          deactivated(){
            console.log("UserPage deactivated");
          }  
      };
      
      

      本地演示视频无法上传,大概意思就是演示被缓存的组件可以记住筛选条件。

    一直以来,自己只知道如何使用这个组件,但对于它的具体实现过程却一无所知。于是我下去参考了其源码,整理了自己的一些收获,如有不恰当之处还望各位大佬指正。

  • 具体实现
    • 附上源码

      // src/core/components/keep-alive.ts
      export default {
        name: 'keep-alive',
        abstract: true, // 为true代表抽象组件
        props: {
          include: patternTypes,
          exclude: patternTypes,
          max: [String, Number]
        },
        methods: {
          cacheVNode() {
            const { cache, keys, vnodeToCache, keyToCache } = this
            if (vnodeToCache) {
              const { tag, componentInstance, componentOptions } = vnodeToCache
              cache[keyToCache] = {
                name: _getComponentName(componentOptions),
                tag,
                componentInstance
              }
              keys.push(keyToCache)
              // prune oldest entry
              if (this.max && keys.length > parseInt(this.max)) {
                pruneCacheEntry(cache, keys[0], keys, this._vnode)
              }
              this.vnodeToCache = null
            }
          }
        },
        created() {
          this.cache = Object.create(null)
          this.keys = []
        },
        destroyed() {
          for (const key in this.cache) {
            pruneCacheEntry(this.cache, key, this.keys)
          }
        },
        mounted() {
          this.cacheVNode()
          this.$watch('include', val => {
            pruneCache(this, name => matches(val, name))
          })
          this.$watch('exclude', val => {
            pruneCache(this, name => !matches(val, name))
          })
        },
        updated() {
          this.cacheVNode()
        },
        render() {
          const slot = this.$slots.default
          console.log("keep-alive:",slot);
          const vnode = getFirstComponentChild(slot)
          const componentOptions = vnode && vnode.componentOptions
          if (componentOptions) {
            // check pattern
            const name = _getComponentName(componentOptions)
            const { include, exclude } = this
            if (
              // not included
              (include && (!name || !matches(include, name))) ||
              // excluded
              (exclude && name && matches(exclude, name))
            ) {
              return vnode
            }
      
            const { cache, keys } = this
            const key =
              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
              remove(keys, key)
              keys.push(key)
            } else {
              // delay setting the cache until update
              this.vnodeToCache = vnode
              this.keyToCache = key
            }
            // @ts-expect-error can vnode.data can be undefined
            vnode.data.keepAlive = true
          }
          return vnode || (slot && slot[0])
        }
      }
      

      与其他的内置组件不同的是,keep-alive组件没有<template>标签,所以其自身不会渲染成一个dom节点。

    • 代码拆分

      ....
      props: {
          include: patternTypes,
          exclude: patternTypes,
          max: [String, Number]
        },
        ....
        // 
        const patternTypes: Array<Function> = [String, RegExp, Array]
      

      props对象声明3个属性,其中include代表需要被缓存的组件们、exclude代表不期望被缓存的组件们、max声明可最大缓存的组件数量。patternTypes约束了include和exclude字段值的类型。

       created() {
          this.cache = Object.create(null)
          this.keys = []
        },
        // 这里似乎用map更方便一点
      

      created钩子函数内声明了2个变量,keys变量用于存放cache对象中的所有的key,cache这个对象用于储存组件实例,以key:value的形式进行储存,其中value的类型声明如下

      type CacheEntry = {
        name?: string
        tag?: string
        componentInstance?: Component
      }
      type CacheEntryMap = Record<string, CacheEntry | null>
      const cache:CacheEntryMap;
      

      组件内很重要的render函数会根据当前组件和缓存中的组件来决定最终要渲染的组件。当keep-alive实例缓存的组件被激活时会触发render函数。

      // 获取keep-alive实例的默认插槽(数组),即放在<keep-alive>标签内部的组件
      const slot = this.$slots.default
      
      // 取出第一个子节点(keep-alive只会渲染一个组件)
      const vnode = getFirstComponentChild(slot)
      
      // 获取组件实例化时传入的options,即.vue组件export default默认导出的配置对象
      const componentOptions = vnode && vnode.componentOptions
      

      通过上面的三行代码我们已经获得了keep-alive组件的第一个子节点的,当配置项componentOptions不为空时,我们得判断这个组件是否需要缓存。

      const name = _getComponentName(componentOptions)
      const { include, exclude } = this
      if ( // 组件不在期望缓存的组件列表内
           (include && (!name || !matches(include, name))) ||
           //组件在不期望被缓存的组件列表内
          (exclude && name && matches(exclude, name))
          ) 
          {
              // 不进行缓存
              return vnode
          }
          
       // _getComponentName
       function _getComponentName(opts?: VNodeComponentOptions): string | null {
         // 优先获取配置项options的name属性值,其次是标签名
        return opts && (getComponentName(opts.Ctor.options as any) || opts.tag)
      }
      

      上面的代码描述了组件的标志(name或tag)匹配到实例exclude中的元素,此时直接返回该虚拟dom。

      const { cache, keys } = this
      const key = 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
          // LRU缓存策略
          remove(keys, key)
          keys.push(key)
      }else { // 未命中缓存
      
              // 保存组件vnode和key 等到触发update钩子时再缓存
            this.vnodeToCache =  vnode 
            this.keyToCache = key
         }
      

      首先获取组件的key,如果cache中key对应的value值不为空,那么代表已经命中了缓存,这时我们只需要取出key值对应的componentInstance,因为keep-alive有规定最大缓存数,当缓存的组件个数大于等于max时,借助LRU算法的思想删掉那些最近很少访问的组件实例,所以当组件命中了缓存策略之后,需要更新一下该组件的key在keep-alive的keys数组的里面的下标。

      下图是LRU策略思想的简图。

      image

      cacheVNode方法用来缓存组件

       methods: {
          cacheVNode() {
            const { cache, keys, vnodeToCache, keyToCache } = this
            if (vnodeToCache) {
              const { tag, componentInstance, componentOptions } = vnodeToCache
              cache[keyToCache] = {
                name: _getComponentName(componentOptions),
                tag,
                componentInstance
              }
              keys.push(keyToCache)
              // LRU缓存策略
              if (this.max && keys.length > parseInt(this.max)) {
                pruneCacheEntry(cache, keys[0], keys, this._vnode)
              }
              // 重置keep-alive实例的vnodeToCache
              this.vnodeToCache = null
            }
          }
        },
      

      从keep-alive实例上获取到需要缓存的组件的vnode,从vnode中我们可以拿到一些关于虚拟节点vnode的属性,然后我们更新cache中key属性的value值,同时将组件的key放进keys数组中,若缓存栈已满则需要删除keys数组中第一个key(LRU缓存策略思想)。

      mounted() {
          this.cacheVNode();// 缓存组件
          this.$watch('include', val => {
            pruneCache(this, name => matches(val, name)) // 判断组件是否期望被缓存
          })
          this.$watch('exclude', val => {
            pruneCache(this, name => !matches(val, name))// 判断组件是否不应该被缓存
          })
        },
      

      mounted钩子里面调用了cacheVNode方法进行缓存组件实例,同时监听了keep-alive实例的include和exclude属性值(即缓存规则)的变化,若传入的值发生变化则调用pruneCache方法。

      function pruneCache(
        keepAliveInstance: { cache: CacheEntryMap; keys: string[]; _vnode: VNode },
        filter: Function
      ) {
        const { cache, keys, _vnode } = keepAliveInstance
        for (const key in cache) {
          const entry = cache[key]
          if (entry) {
            const name = entry.name
            if (name && !filter(name)) { // 注意这里有取反操作符!,删除不满足缓存规则的组件
              pruneCacheEntry(cache, key, keys, _vnode)
            }
          }
        }
      }
      
      function pruneCacheEntry(
        cache: CacheEntryMap,
        key: string,
        keys: Array<string>,
        current?: VNode  //current代表当前处于活跃状态的组件
      ) {
        const entry = cache[key]
        // 判断entry代表的组件是否是当前处于渲染状态的组件
        if (entry && (!current || entry.tag !== current.tag)) {
          // @ts-expect-error can be undefined
          entry.componentInstance.$destroy()// 调用vue内置组件Component实例的销毁方法
        }
        cache[key] = null
        remove(keys, key)
      }
      

      在该函数内对keep-alive的cache对象进行遍历,取出各组件的key值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry方法将这个已经不需要缓存的组件实例先销毁掉,然后再将其从cache、keys中删除。

      updated() {
          this.cacheVNode()
        },
      

      当keep-alive缓存的组件实例状态发生变化时(即触发其activated或deactivated钩子时),keep-alive会重新执行cacheVNode方法执行缓存逻辑。

      destroyed() {
          for (const key in this.cache) {
            pruneCacheEntry(this.cache, key, this.keys)
          }
        },
      

      destroyed钩子函数会在 keep-alive实例被销毁时触发,会遍历cache中存储的组件实例,和当前处于渲染状态的组件进行对比,删除处于非活跃状态的缓存实例。

Posted on 2023-09-20 16:01  易烊千玺圈外女友  阅读(74)  评论(0)    收藏  举报