Vue 緩存

本文是對(duì) vue-element-admin 源碼研究,根據(jù)項(xiàng)目中緩存方面和 Tagviews 實(shí)現(xiàn),進(jìn)行改進(jìn),同時(shí)研究 Vue 內(nèi)置組件 keep-alive 的用法和存在問題。

基礎(chǔ)

keep-alive 基礎(chǔ)文檔、API 文檔

其中需要注意以下幾點(diǎn):

  1. keep-alive 本質(zhì)是把應(yīng)該銷毀的組件緩存起來,當(dāng)再次需要的時(shí)候去讀取緩存的組件信息而不是重新渲染,所以 keep-alive 必須包裹一個(gè)組件才能生效。

  2. 使用了 include and exclude 會(huì)按照這個(gè)規(guī)則進(jìn)行匹配緩存那些頁面,不使用會(huì)緩存所有。

  3. 如果使用了第二條的篩選規(guī)則,那么必須配置對(duì)照和 name,不然無法正確緩存。

文檔原句:
匹配首先檢查組件自身的 name 選項(xiàng),如果 name 選項(xiàng)不可用,則匹配它的局部注冊(cè)名稱 (父組件 components 選項(xiàng)的鍵值)。匿名組件不能被匹配。

  1. keep-alive 內(nèi)部的 router-view ,填寫 key 的時(shí)候,需要謹(jǐn)慎 ,不然會(huì)出現(xiàn)問題。

比如在編輯信息的時(shí)候,用戶打開了兩個(gè)標(biāo)簽頁使用了同一個(gè)組件,不使用 key 就會(huì)復(fù)用這同一個(gè)組件 但是我們需要的是渲染兩個(gè),使用不同的 key 就會(huì)分別渲染兩個(gè),而有時(shí)候 key 又會(huì)生成多余的頁面。

  1. 取消緩存頁面只需要把 include and exclude 中不需要緩存的 name 刪除即可,因?yàn)樵创a中會(huì)監(jiān)聽這個(gè)兩個(gè)字段,刪除緩存的組件。
  mounted () {
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  }

src/core/components/keep-alive 74-81

vue-element-admin 中的緩存

默認(rèn)只實(shí)現(xiàn)了一層緩存,對(duì)緩存頁面進(jìn)行刷新、刪除等操作。

定一個(gè)目標(biāo)

  1. 實(shí)現(xiàn)多層嵌套下,對(duì)頁面進(jìn)行緩存,同時(shí)可以進(jìn)行刪除、刷新。
  2. 動(dòng)態(tài)路由 可打開多個(gè)并同時(shí)進(jìn)行分別緩存。

開始

本篇使用 include 對(duì)緩存頁面進(jìn)行新增和刪除,不考慮默認(rèn)全部緩存的情況

嵌套緩存的實(shí)現(xiàn)

本文例子使用了三層路由:App.vueMain.vue (布局) 、其他第三層路由,只有第二層和第三層啟動(dòng)了緩存,稱為 第一層緩存和第二層緩存 。

緩存路由樹的實(shí)現(xiàn)

參照了 vue-element-admin 中 tagsViews 的實(shí)現(xiàn)在 Vuex 中生成了一個(gè)一維數(shù)組,實(shí)現(xiàn)一層緩存。

https://github.com/PanJiaChen/vue-element-admin/blob/v4.0.0/src/store/modules/tagsView.js

想要實(shí)現(xiàn)多層嵌套緩存 必須建立多維數(shù)組

經(jīng)過實(shí)驗(yàn)和思考后使用 this.$route.matched 對(duì)路由信息進(jìn)行轉(zhuǎn)化為樹形結(jié)構(gòu)

matched介紹

const regex = /\/:\w+/g;
/**
 * 把 matched 格式化為樹形格式
 * @param {Array} matched
 * @param {String} name
 */
function formatMatched(matched, name, parent, path) {
  let route = {
    name: "",
    parent
  };
  matched = matched.slice(1);
  route.name = matched[0].name;
  if (regex.test(matched[0].path)) {
    route.many = true;
  }
  if (matched.length == 1) {
    route.path = path;
  }
  if (matched[0].name !== name) {
    route.children = [].concat(formatMatched(matched, name, route, path));
  }
  return route;
}

一個(gè)節(jié)點(diǎn)的數(shù)據(jù)信息為

{
name: "", //組件的name 主要用于 inclues
path: "", // 區(qū)分相同 name 的 頁面
many:boolean,//是否是動(dòng)態(tài)路由
children:[], //子類
parent: [] // 父類映射,用于刪除和修改,每次修改刪除都遍歷整個(gè)樹太消耗性能了
}

每次切換頁面都會(huì)生成一個(gè)當(dāng)前路由信息的 單分支樹 與總樹進(jìn)行 diff 合并或刪除

新增

/**
 * 新增一個(gè)緩存節(jié)點(diǎn)
 */
function addCached({ cachedViews }, view) {
  let { matched, name, path } = view;
  if (!matched) return;
  const format = formatMatched(matched, name, cachedViews, path);
  mergeCached(cachedViews, format);
}

/**
 * 合并 cache
 */
function mergeCached(all, format) {
  let index = all.findIndex(v => v.name === format.name);
  if (index == -1) {
    all.push(format);
  } else {
    if (format.children && format.children.length) {
      mergeCached(all[index].children, format.children[0]);
    } else {
      //如果是動(dòng)態(tài)路由則可以添加多個(gè),在銷毀的時(shí)候只有全部關(guān)閉才會(huì)取消緩存
      if (
        format.many &&
        format.path &&
        all.findIndex(v => v.path === format.path) === -1
      ) {
        all.push(format);
      }
    }
  }
}

刪除

/**
 *
 * @param {*} param
 * @param {*} view
 */
function removeCached({ cachedViews }, view) {
  let { matched, name, path } = view;
  if (!matched) return;
  const format = formatMatched(matched, name, cachedViews, path);
  delCached(cachedViews, format);
}

function delCached(all, format) {
  let index = all.findIndex(v => {
    if (v.path && format.path) {
      return v.name === format.name && v.path === format.path;
    } else {
      return v.name === format.name;
    }
  });
  if (index == -1) {
    return;
  } else {
    if (format.children && format.children.length) {
      delCached(all[index].children, format.children[0]);
    } else {
      let parent = all[index].parent;
      all.splice(index, 1);
      if (!all.length && !Array.isArray(parent)) {
        delParentCached(parent);
      }
    }
  }
}

在使用的時(shí)候根據(jù)這一棵總數(shù)獲取想要的樹形獲取想要的數(shù)據(jù),比如第一層節(jié)點(diǎn) name 獲取 使用 Vuex 的 Getter

 cachedViews: state => state.tagsView.cachedViews.map(v => v.name),

獲取第二層的緩存 name

  findCachedByName: state => name => {
    let children = state.tagsView.cachedViews.find(v => v.name === name);
    if (!children) {
      return [];
    }
    return children.children
      .map(v => v.name)
      .filter((v, i, a) => a.indexOf(v) === i);
  },

獲取其他層次的 name 需要另行封裝,但是項(xiàng)目中最多也就是實(shí)現(xiàn)三層路由,進(jìn)行兩層緩存,所以目前不考慮。

到此緩存數(shù)據(jù)樹已經(jīng)實(shí)現(xiàn),但是在頁面中的操作還有很多坑,和其解決思路。

頁面中的設(shè)置

在第一層緩存中使用 Vuex 的 Getter 獲取 cachedViews

<keep-alive :include="cachedViews">
    <router-view :key="key" />
</keep-alive>
<script>
import { mapGetters } from "vuex";
export default {
  name: "AppMain",
  computed: {
    ...mapGetters(["cachedViews"]),
    key() {
      if (this.$route.matched.length > 1) {
        return this.$route.matched[1].path;
      } else {
        return this.$route.path;
      }
    }
  }
};
</script>

key 必不可少 , 如果 路由嵌套層次大于等于1 就取 matched 的第二層 path,因?yàn)槲覀儺?dāng)前是第二層路由,第一層是 App.vue , 如果等于第二層就取當(dāng)前的路由 path

在第二層緩存中使用 Vuex Getter 的函數(shù)形式獲取確定的緩存頁面 name

<template>
  <keep-alive :include="include">
    <router-view :key="key" />
  </keep-alive>
</template>
<script>
export default {
  name: "authorityAuth",
  data() {
    return {
      key: this.$route.path
    };
  },
  computed: {
    include() {
      return this.$store.getters.findCachedByName("authorityAuth");
    }
  },
  watch: {
    $route(v) {
      if (v.name.includes("authorityAuth")) {
        this.key = v.path;
      }
      if (this.include.length === 0) {
        this.key === undefined;
      }
    }
  }
};
</script>

在第二層的緩存的時(shí)候,key值處理比較復(fù)雜,原本是直接使用this.$route.path,但是出現(xiàn)了非常致命的問題。

主要原因是:

Vue 緩存的頁面,由于屬性劫持的原因,即使被緩存了,$route的變化還會(huì)觸發(fā)變化,$route變化,觸發(fā)了 key 的變化 從而制造多余無意義的頁面如下:

只有第一個(gè)頁面時(shí)需要的

組件被緩存后,由于 key 值綁定 $route.path 當(dāng)頁面切換時(shí),key發(fā)生改變會(huì)創(chuàng)建大量的無用頁面占用內(nèi)存,導(dǎo)致頁面迅速卡死。

所以引出一個(gè)問題,緩存的頁面是否需要繼續(xù)活躍屬性變化,但是數(shù)據(jù)劫持是 Vue 的核心,目前沒有任何辦法能從根源解決,即,短時(shí)間凍結(jié)劫持。

目前解決方法是在第三層 <route-view /> 中緩存 key ,只有當(dāng)前頁面切換是當(dāng)前的緩存的子頁面才會(huì)改變 key。

小結(jié)

通過這種方式,可以在一定程度上實(shí)現(xiàn)多層緩存和刪除,但是如果牽扯到緩存的刷新和動(dòng)態(tài)路由緩存等問題,就會(huì)發(fā)現(xiàn) keep-alive 存在的很多缺陷,下面會(huì)一一介紹.

當(dāng)前思路下其他的嵌套緩存方案(廢棄)

在嘗試嵌套緩存的時(shí)候,還進(jìn)行了其他的嘗試:

這種方案本質(zhì)是 直接在 vue-element-admin 緩存方案中直接套用 嵌套緩存,并非參照系統(tǒng)的本身問題,因?yàn)?vue-element-admin 本身需求就是緩存一層。

這種方案本質(zhì)還是在于 key 的處理上 ,在上文的基礎(chǔ)上進(jìn)行一點(diǎn)點(diǎn)修改:

  1. 在第一層緩存中 key 值總是取最底層的 path 即 this.$route.path ,試想一下,無論是二層嵌套路由或者是三層嵌套路由,永遠(yuǎn)都是最底層的 path ,表現(xiàn)結(jié)果是:
造成了更大的性能問題!

由上圖可以看到 造成了更加嚴(yán)重的性能問題!

有兩點(diǎn)困難之處:

  1. 上文說的 緩存頁面內(nèi)部的劫持依然活躍 key 的變化創(chuàng)造了更多的無用頁面。
  2. 由于每一個(gè)二級(jí)緩存都創(chuàng)建了 AuthorityAuth 組件, 也就造成了 無法刪除緩存,因?yàn)樗鼈兊?name 都是 AuthorityAuth ,刪除一個(gè)就換導(dǎo)致全部緩存刪除。

keep-alive 確定緩存是以 name 為基準(zhǔn)的 ,這導(dǎo)致在在一個(gè)組件創(chuàng)建不同的 key 達(dá)到 復(fù)用,比如緩存多個(gè)動(dòng)態(tài)路由 ,無法精準(zhǔn)的刪除某一個(gè)頁面。

動(dòng)態(tài)組件緩存問題

這個(gè)問題和上一段寫的問題是同一個(gè),由于動(dòng)態(tài)路由,使用的同一個(gè)組件,name 是相同的,我們可以通過 key 打開多個(gè)頁面,但是我們卻沒辦法精準(zhǔn)的控制每個(gè)頁面的緩存和刷新。

我們只能實(shí)現(xiàn):全部關(guān)閉后全部清空。

遺留的問題還有:一個(gè)刷新,則全部刷新

目前實(shí)現(xiàn)是打開多個(gè)無法刷新,因?yàn)?,為了?shí)現(xiàn)全部關(guān)閉后取消緩存,也就是說在緩存樹中會(huì)創(chuàng)建多個(gè) name 相同,但是 path 不同的緩存信息,最后再去重得到 include。

其他緩存思路

網(wǎng)上還有很多大佬有很多的想法來實(shí)現(xiàn)緩存頁面,大致可分:

  1. 默認(rèn)緩存所有,手動(dòng)調(diào)用 vm.$destroy() 注銷組件。

  2. 通過查詢 Vnode 找到 keep-alive 的 cache 手動(dòng)刪除緩存。

  3. 不使用 keep-alive 頁面切換保存 data 的屬性。

等等等。。。。

但是我感覺還是使用 keep-alive 比較好,但是 keep-alive 擁有兩個(gè)缺陷 。

keep-alive 的局限性

  1. 緩存的頁面內(nèi)部使用的劫持屬性還是活躍的,這會(huì)導(dǎo)致其他頁面的操作影響緩存的頁面,比如 key 值綁定問題。

  2. keep-alive 在緩存 動(dòng)態(tài)路由的問題,相同的 name 可以使用 key 創(chuàng)建不同的 實(shí)例,但是我們只能用 name 去操作這一系列頁面 。

總結(jié)

如果不考慮以上出現(xiàn)的問題,那么本文還是可以解決,一般遇到的所有緩存問題。

源碼

參考資料

Vue Key

一句話來說就是不同的 key 會(huì)被 Vue 當(dāng)成不同的元素,即使是使用了相同的組件,會(huì)被創(chuàng)建多份,這在配合路由和緩存使用時(shí)尤其重要。

Vue 緩存

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容