关于前端:嘿vue中keepalive有个大坑你可能还不知道

38次阅读

共计 5048 个字符,预计需要花费 13 分钟才能阅读完成。

背景

背景是这样的,咱们应用 vue2 开发一个在线客服应用的 IM 利用,根本布局是右边是访客列表,左边是访客对话,为了让对话加载更敌对,咱们将对话的路由应用 <keep-alive> 缓存起来。然而如果将所有对话都缓存,未必会造成缓存过多卡顿的问题。天然,就应用上了 <keep-alive> 提供的 max 属性,设置一个缓存对话内容组件下限,依照 LRU 算法,会销毁最旧拜访的组件,保留最近应用的组件。本认为美妙如期而至,直到上线后翻大车了,实在对话量大了,内存飙升卡顿。起初具体分析内存增长点,通过 vuedevtool查看组件树,发现对话内容组件始终是递增,并非维持在 max 设置的数量下限!
各位看官稍安勿躁,上面就具体分析造成这个「大坑」的原理,曾经解决它的计划。

情景模仿

为了不便模仿背景案例,这里就用 vue2 简略的写一个 demo。
对话列表组件 APP.vue,点击列表中的某个访客,加载与访客对话内容。

<template>
  <div id="app">
    <section class="container">
      <aside class="aside">
        <ul>
          <li :class="{active: active === index}" v-for="(user, index) in userList" :key="index"
            @click="selectUser(index, user)">
            {{user.name}}
          </li>
        </ul>
      </aside>
      <section class="main">
        <keep-alive :max="3">
          <chat-content :key="currentUser.id" :user-info="currentUser"></chat-content>
        </keep-alive>
      </section>
    </section>
  </div>
</template>

<script>
import ChatContent from './views/ChatContent.vue';
export default {
  components: {ChatContent},
  data() {
    return {
      active: -1,
      currentUser: {},
      userList: [{id: 1, name: "张三"},
      {id: 2, name: "李四"},
      {id: 3, name: "王五"},
      {id: 4, name: "老六"},
      {id: 5, name: "老八"},
      {id: 6, name: "老九"},
    ]
    }
  },
  methods: {selectUser(index) {
      this.active = index
      this.currentUser = this.userList[index];
    }
  },
}
</script>

这里应用 keep-alive 组件包裹的对话内容组件,须要加上 key 惟一标记,这样才会缓存雷同名称(不同 key)的组件,否则不会缓存。
对话内容组件ChatContent.vue,简略加一个计数器验证组件缓存了。

<template>
  <div>
    <h2>{{userInfo.name}}</h2>
    <h3>{{num}}</h3>
    <button @click="increament">+1</button>
  </div>
</template>

<script>
export default {
  props: {userInfo: Object,},

  data() {
    return {num: 0,};
  },

  methods: {increament() {this.num += 1;},
  },
};
</script>

情景模仿后果

试验发现,尽管缓存组件个数下限 max 为 3,理论是一一缓存了全部内容组件,看来设置 max 属性生效了。

Vue2 中 <keep-alive> 组件实现原理

为什么缓存雷同名称的组件,max属性会生效呢?这里就要从 Vue2<keep-alive>组件实现原理来看。

<keep-alive>LRU 算法

  • vue会将 VNode 及组件实例(componentInstance)存到缓存(cache),cache是一个 Object,同时还会保护一个keys 队列;
  • 依据 LRU 算法对 cachekeys的治理:以后激活组件已存在缓存中,将组件对应 key 先删除,再插入的形式往前挪动;
  • 以后激活组件没有再缓存中,间接存入缓存,此时判断是否超过了缓存个数下限,如果超过了,应用 pruneCacheEntry 清理 keys 第一个地位(最旧)的组件对应的缓存。

    if (cache[key]) {vnode.componentInstance = cache[key].componentInstance;
    // make current key freshest
    remove(keys, key);
    keys.push(key);
    } else {cache[key] = vnode;
    keys.push(key);
    // prune oldest entry
    if (this.max && keys.length > parseInt(this.max)) {pruneCacheEntry(cache, keys[0], keys, this._vnode);
      console.log('cache:', cache)
      console.log('keys:', keys)
    }
    }

    <keep-alive> 清理缓存函数实现

    上面再来看清理缓存函数 pruneCacheEntry 的实现:比对以后传入组件和在缓存中的组件 tag 是否雷同,如果不同,就去销毁组件实例,否则不销毁。

    function pruneCacheEntry (
    cache,
    key,
    keys,
    current
    ) {var cached$$1 = cache[key];
    if (cached$$1 && (!current || cached$$1.tag !== current.tag)) {cached$$1.componentInstance.$destroy();
    }
    cache[key] = null;
    remove(keys, key);
    }

    看到这里仿佛也没有故障,到底是哪里出问题了呢?

    <keep-alive> 源码调试发现问题

    无妨咱们打印一下 cacheVNode 缓存)和keys

    发现也没什么问题,依照 URL 算法失去正确后果。
    再看看清理缓存函数里 cached$$1.tagcurrent.tag的打印

    假相了!他两因为组件名称雷同,导致相等,没有进入销毁组件实例的判断里,这就是问题起源!为什么针对雷同组件名称不去销毁实例呢?可能是为了某些情景下组件复用吧。

    解决方案

    既然问题症结咱们曾经找到,从源头下来解决问题当然最佳,然而事实是 vue2 源码层面是没有去解决的(vue3有解决,这个前面再说),只能从咱们利用侧再去想想方法。这里我想到的有两种计划。

    计划一:剪枝法

    保护一个全局状态(比方 vuex)对话ids 队列,最大长度为 max,相似vueLRU算法中的 keys,在组件activated 钩子函数触发时更新 ids 队列。对话内容组件的子组件判断以后对话 id 是否在 ids 队列中,不在那么就会 v-if 剔除,否则缓存起来,这样很大水平水平上开释缓存。相似剪去树的枝丫,加重分量,这里叫做「剪枝法」好了。

    计划二:自定义清理缓存函数

    咱们不再应用 keep-alive 提供的 max 属性来清理缓存,让其将组件实例全副缓存下来,以后激活组件,activated钩子函数触发,此时通过 this.$vnode.parent.componentInstance 获取组件实例,进而能够获取挂载在下面的 cachekeys。这样咱们就能够通过 LRU 算法,依据 key 自定义精准清理缓存了。

    activated() {const { cache, keys} = this.$vnode.parent.componentInstance;
    console.log('activated cache:', cache)
    console.log('activated keys:', keys)
    
    let cacheLen = 0
    const max = 3
    Object.keys(cache).forEach(key => {if (cache[key]) {
        cacheLen += 1
        if (cacheLen > max) {const key = keys.shift()
          cache[key].componentInstance.$destroy()
          cache[key] = null
        }
      }
    })
    },

    上面对照 vuedevtool 工具查看成果

    完全符合预期!形式二从 <keep-alive> 组件根就清理了缓存组件,更彻底,对业务代码侵染性也更小。
    你认为这样就完了?下面我还提到在 vue3 中曾经解决了这个问题。

    vue3 中 <KeepAlive> 组件实现原理

    话不多说,先来看下面雷同的案例在应用 vue3 写的成果如何呢?这里就不“反复”贴代码了,间接看 devtool 组件树的体现。

    没有冗余缓存组件,奈斯!

    vue3 中 <KeepAlive>LRU 算法

    vue3LRU 算法实现思路一样,只不过 cachekeys别离应用 MapSet数据结构实现,数据更洁净简洁。

    const cache = new Map();
    const keys = new Set();
    // ...
    if (cachedVNode) {
      // copy over mounted state
      vnode.el = cachedVNode.el;
      
      // ...
      // make this key the freshest
      keys.delete(key);
      keys.add(key);
    }
    else {keys.add(key);
      // prune oldest entry
      if (max && keys.size > parseInt(max, 10)) {pruneCacheEntry(keys.values().next().value);
      }
    }

    vue3 中 <KeepAlive> 清理缓存函数实现

    vue3中清理组件实例缓存函数也是 pruneCacheEntry,不同的是,比对以后传入组件和在缓存中的组件tag 是否雷同,决定是否销毁组件实例。

    function pruneCacheEntry(key) {const cached = cache.get(key);
    if (!current || cached.type !== current.type) {unmount(cached);
    }
    else if (current) {
        // current active instance should no longer be kept-alive.
        // we can't unmount it now but it might be later, so reset its flag now.
        resetShapeFlag(current);
    }
    cache.delete(key);
    keys.delete(key);
    }

    再来看看 cache.typecurrent.type到底是什么

    比照咱们会发现,不再是简略的组件名称字符标记,而是一个对象形容,蕴含了很多属性,因为在初始化组件实例时,会给每个实例加上属性:propsrendersetup__hmrId等。

    function initProps(instance, rawProps, isStateful, isSSR = false) {const props = {};
    const attrs = {};
    def(attrs, InternalObjectKey, 1);
    instance.propsDefaults = /* @__PURE__ */ Object.create(null);
    setFullProps(instance, rawProps, props, attrs);
    // ...  
    instance.attrs = attrs;
    }
    function isInHmrContext(instance) {while (instance) {if (instance.type.__hmrId)
        return true;
      instance = instance.parent;
    }
    }

    即便是对象中所有属性雷同,然而对象不是同一个援用地址,造成 cache.typecurrent.type不相等,因而会销毁实例对象 unmount(cached)。以上就是vue3 对这个问题解决方案。

    总结

    最初,在 vue2 中会呈现 <keep-alive> 缓存雷同名称组件,max生效的问题,举荐应用自定义清理缓存函数,在获取组件实例根底上,对缓存实例销毁。下图是我在实在我的项目中优化的成绩。完~

正文完
 0