前端進階演算法3:從瀏覽器快取淘汰策略和Vue的keep-alive學習LRU演算法

前端瓶子君發表於2020-04-07

前端進階演算法3:從瀏覽器快取淘汰策略和Vue的keep-alive學習LRU演算法

引言

這個標題已經很明顯的告訴我們:前端需要了解 LRU 演算法!

這也是前端技能的亮點,當面試官在問到你前端開發中遇到過哪些演算法,你也可以把這部分丟過去!

本節按以下步驟切入:

  • 由瀏覽器快取策略引出 LRU 演算法原理
  • 然後走進 vuekeep-alive 的應用
  • 接著,透過 vuekeep-alive 原始碼看 LRU 演算法的實現
  • 最後,來一道 leetcode 題目,我們來實現一個 LRU 演算法

按這個步驟來,完全掌握 LRU 演算法,點亮前端技能,下面就開始吧?

一、LRU 快取淘汰策略

快取在計算機網路上隨處可見,例如:當我們首次訪問一個網頁時,開啟很慢,但當我們再次開啟這個網頁時,開啟就很快。

這就涉及快取在瀏覽器上的應用:瀏覽器快取。當我們開啟一個網頁時,例如 https://github.com/sisterAn/JavaScript-Algorithms ,它會在發起真正的網路請求前,查詢瀏覽器快取,看是否有要請求的檔案,如果有,瀏覽器將會攔截請求,返回快取檔案,並直接結束請求,不會再去伺服器上下載。如果不存在,才會去伺服器請求。

其實,瀏覽器中的快取是一種在本地儲存資源副本,它的大小是有限的,當我們請求數過多時,快取空間會被用滿,此時,繼續進行網路請求就需要確定快取中哪些資料被保留,哪些資料被移除,這就是瀏覽器快取淘汰策略,最常見的淘汰策略有 FIFO(先進先出)、LFU(最少使用)、LRU(最近最少使用)。

LRU ( Least Recently Used :最近最少使用 )快取淘汰策略,故名思義,就是根據資料的歷史訪問記錄來進行淘汰資料,其核心思想是 如果資料最近被訪問過,那麼將來被訪問的機率也更高 ,優先淘汰最近沒有被訪問到的資料。

畫個圖幫助我們理解:

前端進階演算法3:從瀏覽器快取淘汰策略和Vue的keep-alive學習LRU演算法

二、LRU 在 keep-alive (Vue) 上的實現

1. keep-alive

keep-alive 在 vue 中用於實現元件的快取,當元件切換時不會對當前元件進行解除安裝。

<!-- 基本 -->
<keep-alive>
  <component :is="view"></component>
</keep-alive>
複製程式碼

最常用的兩個屬性:includeexculde ,用於元件進行有條件的快取,可以用逗號分隔字串、正規表示式或一個陣列來表示。

在 2.5.0 版本中,keep-alive 新增了 max 屬性,用於最多可以快取多少元件例項,一旦這個數字達到了,在新例項被建立之前,已快取元件中最久沒有被訪問的例項會被銷燬掉,看,這裡就應用了 LRU 演算法。即在 keep-alive 中快取達到 max,新增快取例項會優先淘汰最近沒有被訪問到的例項???

下面我們透過 vue 原始碼看一下具體的實現?

2. 從 vue 原始碼看 keep-alive 的實現

export default {
  name: "keep-alive",
  // 抽象元件屬性 ,它在元件例項建立父子關係的時候會被忽略,發生在 initLifecycle 的過程中
  abstract: true, 
  props: {
    // 被快取元件
    include: patternTypes, 
    // 不被快取元件
    exclude: patternTypes,
    // 指定快取大小
    max: [String, Number] 
  },
  created() {
    // 初始化用於儲存快取的 cache 物件
    this.cache = Object.create(null);
    // 初始化用於儲存VNode key值的 keys 陣列
    this.keys = []; 
  },
  destroyed() {
    for (const key in this.cache) {
      // 刪除所有快取
      pruneCacheEntry(this.cache, key, this.keys);
    }
  },
  mounted() {
    // 監聽快取(include)/不快取(exclude)元件的變化
    // 在變化時,重新調整 cache
    // pruneCache:遍歷 cache,如果快取的節點名稱與傳入的規則沒有匹配上的話,就把這個節點從快取中移除
    this.$watch("include", val => {
      pruneCache(this, name => matches(val, name));
    });
    this.$watch("exclude", val => {
      pruneCache(this, name => !matches(val, name));
    });
  },
  render() {
    // 獲取第一個子元素的 vnode
    const slot = this.$slots.default;
    const vnode: VNode = getFirstComponentChild(slot);
    const componentOptions: ?VNodeComponentOptions =
      vnode && vnode.componentOptions;
    if (componentOptions) {
      // name 不在 inlcude 中或者在 exlude 中則直接返回 vnode,否則繼續進行下一步
      // check pattern
      const name: ?string = 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;
      // 獲取鍵,優先獲取元件的 name 欄位,否則是元件的 tag
      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;
        
      // --------------------------------------------------
      // 下面就是 LRU 演算法了,
      // 如果在快取裡有則調整,
      // 沒有則放入(長度超過 max,則淘汰最近沒有訪問的)
      // --------------------------------------------------
      // 如果命中快取,則從快取中獲取 vnode 的元件例項,並且調整 key 的順序放入 keys 陣列的末尾
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        remove(keys, key);
        keys.push(key);
      }
      // 如果沒有命中快取,就把 vnode 放進快取
      else {
        cache[key] = vnode;
        keys.push(key);
        // prune oldest entry
        // 如果配置了 max 並且快取的長度超過了 this.max,還要從快取中刪除第一個
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }
      
      // keepAlive標記位
      vnode.data.keepAlive = true;
    }
    return vnode || (slot && slot[0]);
  }
};

// 移除 key 快取
function 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)
}

// remove 方法(shared/util.js)
/**
 * Remove an item from an array.
 */
export function remove (arr: Array<any>, item: any): Array<any> | void {
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}
複製程式碼

keep-alive原始碼路徑

keep-alive 快取超過 max 時,使用的快取淘汰演算法就是 LRU 演算法,它在實現的過程中用到了 cache 物件用於儲存快取的元件例項及 key 值,keys 陣列用於儲存快取元件的 key ,當 keep-alive 中渲染一個需要快取的例項時:

  • 判斷快取中是否已快取了該例項,快取了則直接獲取,並調整 keykeys 中的位置(移除 keyskey ,並放入 keys 陣列的最後一位)
  • 如果沒有快取,則快取該例項,若 keys 的長度大於 max (快取長度超過上限),則移除 keys[0] 快取

下面我們來自己實現一個 LRU 演算法吧⛽️⛽️⛽️

三、leetcode:LRU 快取機制

運用你所掌握的資料結構,設計和實現一個 LRU (最近最少使用) 快取機制。它應該支援以下操作: 獲取資料 get 和寫入資料 put

獲取資料 get(key) - 如果金鑰 ( key ) 存在於快取中,則獲取金鑰的值(總是正數),否則返回 -1 。 寫入資料 put(key, value) - 如果金鑰不存在,則寫入資料。當快取容量達到上限時,它應該在寫入新資料之前刪除最久未使用的資料,從而為新資料留出空間。

進階:

你是否可以在 O(1) 時間複雜度內完成這兩種操作?

示例:

LRUCache cache = new LRUCache( 2 /* 快取容量 */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回  1
cache.put(3, 3);    // 該操作會使得金鑰 2 作廢
cache.get(2);       // 返回 -1 (未找到)
cache.put(4, 4);    // 該操作會使得金鑰 1 作廢
cache.get(1);       // 返回 -1 (未找到)
cache.get(3);       // 返回  3
cache.get(4);       // 返回  4
複製程式碼

前面已經介紹過了 keep-alive 中LRU實現原始碼,現在來看這道題是不是很簡單???,可以嘗試自己解答一下⛽️,然後思考一下有沒有什麼繼續優化的!歡迎提供更多的解法

歡迎將答案提交到 github.com/sisterAn/Ja… ,讓更多人看到,瓶子君也會在明日放上自己的解答。

四、認識更多的前端道友,一起進階前端開發

前端演算法集訓營第一期免費開營啦???,免費喲!

在這裡,你可以和志同道合的前端朋友們(600+)一起進階前端演算法,從0到1構建完整的資料結構與演算法體系。

在這裡,瓶子君不僅介紹演算法,還將演算法與前端各個領域進行結合,包括瀏覽器、HTTP、V8、React、Vue原始碼等。

在這裡,你可以每天學習一道大廠演算法題(阿里、騰訊、百度、位元組等等)或 leetcode,瓶子君都會在第二天解答喲!

更多福利等你解鎖???!

掃碼加入【前端演算法交流群交流群】,若二維碼人數已經達到上限,可掃底部二維碼,在公眾號「前端瓶子君」內回覆「演算法」自動拉你進群學習

前端進階演算法3:從瀏覽器快取淘汰策略和Vue的keep-alive學習LRU演算法
前端進階演算法3:從瀏覽器快取淘汰策略和Vue的keep-alive學習LRU演算法

相關文章