「Vue原始碼學習」簡單講一講keep-alive的原理吧

Sunshine_Lin發表於2021-12-27

前言

大家好,我是林三心,用最通俗易懂講最難的知識點是我的座右銘,基礎是進階的前提是我的初心。

回想起來,我一開始寫作的時候就是寫Vue原始碼系列的,都收錄在我的掘金專欄Vue原始碼解析之中:

今天,就給大家講講Vue中常用的元件 keep-alive 的基本原理吧!

場景

可能大家在平時的開發中會經常遇到這樣的場景:有一個可以進行篩選的列表頁 List.vue ,點選某一項時進入相應的詳情頁面,等到你從詳情頁返回 List.vue 時,發現列表頁居然重新整理了!剛剛的篩選條件都沒了!!!

截圖2021-12-19 上午11.08.50.png

keep-alive

是什麼?

  • keep-alive 是一個 Vue全域性元件
  • keep-alive 本身不會渲染出來,也不會出現在父元件鏈中
  • keep-alive 包裹動態元件時,會快取不活動的元件,而不是銷燬它們

怎麼用?

keep-alive 接收三個引數:

  • include :可傳 字串、正規表示式、陣列 ,名稱匹配成功的元件會被快取
  • exclude :可傳 字串、正規表示式、陣列 ,名稱匹配成功的元件不會被快取
  • max :可傳 數字 ,限制快取元件的最大數量

include exclude ,傳 陣列 情況居多

動態元件

<keep-alive :include="allowList" :exclude="noAllowList" :max="amount"> 
    <component :is="currentComponent"></component> 
</keep-alive>

路由元件

<keep-alive :include="allowList" :exclude="noAllowList" :max="amount">
    <router-view></router-view>
</keep-alive>

原始碼

元件基礎

前面說了, keep-alive 是一個 Vue全域性元件 ,他接收三個引數:

  • include :可傳 字串、正規表示式、陣列 ,名稱匹配成功的元件會被快取
  • exclude :可傳 字串、正規表示式、陣列 ,名稱匹配成功的元件不會被快取
  • max :可傳 數字 ,限制快取元件的最大數量,超過 max 則按照 LRU演算法 進行置換

順便說說 keep-alive 在各個生命週期裡都做了啥吧:

  • created :初始化一個 cache、keys ,前者用來存快取元件的虛擬dom集合,後者用來存快取元件的key集合
  • mounted :實時監聽 include、exclude 這兩個的變化,並執行相應操作
  • destroyed :刪除掉所有快取相關的東西
之前說了, keep-alive 不會被渲染到頁面上,所以 abstract 這個屬性至關重要!
// src/core/components/keep-alive.js

export default {
  name: 'keep-alive',
  abstract: true, // 判斷此元件是否需要在渲染成真實DOM
  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },
  created() {
    this.cache = Object.create(null) // 建立物件來儲存  快取虛擬dom
    this.keys = [] // 建立陣列來儲存  快取key
  },
  mounted() {
    // 實時監聽include、exclude的變動
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },
  destroyed() {
    for (const key in this.cache) { // 刪除所有的快取
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },
  render() {
      // 下面講
  }
}

pruneCacheEntry函式

我們們上面實現的生命週期 destroyed 中,執行了 刪除所有快取 這個操作,而這個操作是通過呼叫 pruneCacheEntry 來實現的,那我們們來說說 pruneCacheEntry 裡做了啥吧

// src/core/components/keep-alive.js

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() // 執行元件的destory鉤子函式
  }
  cache[key] = null  // 設為null
  remove(keys, key) // 刪除對應的元素
}

總結一下就是做了三件事:

  • 1、遍歷集合,執行所有快取元件的 $destroy 方法
  • 2、將 cache 對應 key 的內容設定為 null
  • 3、刪除 keys 中對應的元素

render函式

以下稱 include 為白名單, exclude 為黑名單
render 函式裡主要做了這些事:
  • 第一步:獲取到 keep-alive 包裹的第一個元件以及它的 元件名稱
  • 第二步:判斷此 元件名稱 是否能被 白名單、黑名單 匹配,如果 不能被白名單匹配 || 能被黑名單匹配 ,則直接返回 VNode ,不往下執行,如果不符合,則往下執行 第三步
  • 第三步:根據 元件ID、tag 生成 快取key ,並在快取集合中查詢是否已快取過此元件。如果已快取過,直接取出快取元件,並更新 快取key keys 中的位置(這是 LRU演算法 的關鍵),如果沒快取過,則繼續 第四步
  • 第四步:分別在 cache、keys 中儲存 此元件 以及他的 快取key ,並檢查數量是否超過 max ,超過則根據 LRU演算法 進行刪除
  • 第五步:將此元件例項的 keepAlive 屬性設定為true,這很重要哦,下面會講到的!
// src/core/components/keep-alive.js

render() {
  const slot = this.$slots.default
  const vnode: VNode = getFirstComponentChild(slot) // 找到第一個子元件物件
  const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
  if (componentOptions) { // 存在元件引數
    // 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
    const key: ?string = vnode.key == null // 定義元件的快取key
      // 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) // 調整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)
      }
    }

    vnode.data.keepAlive = true // 渲染和執行被包裹元件的鉤子函式需要用到
  }
  return vnode || (slot && slot[0])
}

渲染

我們們先來看看Vue一個元件是怎麼渲染的,我們們從 render 開始說:

  • render :此函式會將元件轉成 VNode
  • patch :此函式在初次渲染時會直接渲染根據拿到的 VNode 直接渲染成 真實DOM ,第二次渲染開始就會拿 VNode 會跟 舊VNode 對比,打補丁(diff演算法對比發生在此階段),然後渲染成 真實DOM

截圖2021-12-19 下午8.45.25.png

keep-alive本身渲染

剛剛說了, keep-alive 自身元件不會被渲染到頁面上,那是怎麼做到的呢?其實就是通過判斷元件例項上的 abstract 的屬性值,如果是 true 的話,就跳過該例項,該例項也不會出現在父級鏈上

// src/core/instance/lifecycle.js

export function initLifecycle (vm: Component) {
  const options = vm.$options
  // 找到第一個非abstract的父元件例項
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
  vm.$parent = parent
  // ...
}

包裹元件渲染

我們們再來說說被 keep-alive 包裹著的元件是如何使用快取的吧。剛剛說了 VNode -> 真實DOM 是發生在 patch 的階段,而其實這也是要細分的: VNode -> 例項化 -> _update -> 真實DOM ,而元件使用快取的判斷就發生在 例項化 這個階段,而這個階段呼叫的是 createComponent 函式,那我們就來說說這個函式吧:

// src/core/vdom/patch.js

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }

    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm) // 將快取的DOM(vnode.elm)插入父元素中
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}
  • 在第一次載入被包裹元件時,因為 keep-alive render 先於包裹元件載入之前執行,所以此時 vnode.componentInstance 的值是 undefined ,而 keepAlive true ,則程式碼走到 i(vnode, false /* hydrating */) 就不往下走了
  • 再次訪問包裹元件時,vnode.componentInstance的值就是已經快取的元件例項,那麼會執行insert(parentElm, vnode.elm, refElm)邏輯,這樣就直接把上一次的DOM插入到了父元素中。

結語

我是林三心,一個熱心的前端菜鳥程式設計師。如果你上進,喜歡前端,想學習前端,那我們們可以交朋友,一起摸魚哈哈,摸魚群

image.png

參考

相關文章