Vue源碼探究-組件的持久活躍

Vue源碼探究-組件的持久活躍

*本篇代碼位於vue/src/core/components/keep-alive.js

較新版本的Vue增加了一個內置組件 keep-alive,用於存儲組件狀態,即便失活也能保持現有狀態不變,切換回來的時候不會恢復到初始狀態。由此可知,路由切換的鉤子所觸發的事件處理是無法適用於 keep-alive 組件的,那如果需要根據失活與否來給予組件事件通知,該怎麼辦呢?如前篇所述,keep-alive 組件有兩個特有的生命週期鉤子 activateddeactivated,用來響應失活狀態的事件處理。

來看看 keep-alive 組件的實現,代碼文件位於 components 裏,目前入口文件裏也只有 keep-alive 這一個內置組件,但這個模塊的分離,會不會預示着官方將在未來開發更多具有特殊功能的內置組件呢?

// 導入輔助函數
import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'

// 定義VNodeCache靜態類型
// 它是一個包含key名和VNode鍵值對的對象,可想而知它是用來存儲組件的
type VNodeCache = { [key: string]: ?VNode };

// 定義getComponentName函數,用於獲取組件名稱,傳入組件配置對象
function getComponentName (opts: ?VNodeComponentOptions): ?string {
  // 先嚐試獲取配置對象中定義的name屬性,或無則獲取標籤名稱
  return opts && (opts.Ctor.options.name || opts.tag)
}

// 定義matches函數,進行模式匹配,傳入匹配的模式類型數據和name屬性
function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
  // 匹配數組模式
  if (Array.isArray(pattern)) {
    // 使用數組方法查找name,返回結果
    return pattern.indexOf(name) > -1
  } else if (typeof pattern === 'string') {
    // 匹配字符串模式
    // 將字符串轉換成數組查找name,返回結果
    return pattern.split(',').indexOf(name) > -1
  } else if (isRegExp(pattern)) {
    // 匹配正則表達式
    // 使用正則匹配name,返回結果
    return pattern.test(name)
  }
  /* istanbul ignore next */
  // 未匹配正確模式則返回false
  return false
}

// 定義pruneCache函數,修剪keep-alive組件緩存對象
// 接受keep-alive組件實例和過濾函數
function pruneCache (keepAliveInstance: any, filter: Function) {
  // 獲取組件的cache,keys,_vnode屬性
  const { cache, keys, _vnode } = keepAliveInstance
  // 遍歷cache對象
  for (const key in cache) {
    // 獲取緩存資源
    const cachedNode: ?VNode = cache[key]
    // 如果緩存資源存在
    if (cachedNode) {
      // 獲取該資源的名稱
      const name: ?string = getComponentName(cachedNode.componentOptions)
      // 當名稱存在 且不匹配緩存過濾時
      if (name && !filter(name)) {
        // 執行修剪緩存資源操作
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

// 定義pruneCacheEntry函數,修剪緩存條目
// 接受keep-alive實例的緩存對象和鍵名緩存對象,資源鍵名和當前資源
function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  // 檢查緩存對象裏是否已經有以key值存儲的資源
  const cached = cache[key]
  // 如果有舊資源並且沒有傳入新資源參數或新舊資源標籤不同
  if (cached && (!current || cached.tag !== current.tag)) {
    // 銷燬該資源
    cached.componentInstance.$destroy()
  }
  // 置空key鍵名存儲資源
  cache[key] = null
  // 移除key值的存儲
  remove(keys, key)
}

// 定義模式匹配接收的數據類型
const patternTypes: Array<Function> = [String, RegExp, Array]

// 導出keep-alive組件實例的配置對象
export default {
  // 定義組件名稱
  name: 'keep-alive',
  // 設置abstract屬性
  abstract: true,
  // 設置組件接收的屬性
  props: {
    // include用於包含模式匹配的資源,啓用緩存
    include: patternTypes,
    // exclude用於排除模式匹配的資源,不啓用緩存
    exclude: patternTypes,
    // 最大緩存數
    max: [String, Number]
  },

  created () {
    // 實例創建時定義cache屬性爲空對象,用於存儲資源
    this.cache = Object.create(null)
    // 設置keys數組,用於存儲資源的key名
    this.keys = []
  },

  destroyed () {
    // 實例銷燬時一併銷燬存儲的資源並清空緩存對象
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    // DOM加載完成後,觀察include和exclude屬性的變動
    // 回調執行修改緩存對象的操作
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render () {
    // 實例渲染函數
    // 獲取keep-alive包含的子組件結構
    // keep-alive組件並不渲染任何真實DOM節點,只渲染嵌套在其中的組件資源
    const slot = this.$slots.default
    // 將嵌套組件dom結構轉化成虛擬節點
    const vnode: VNode = getFirstComponentChild(slot)
    // 獲取嵌套組件的配置對象
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    // 如果配置對象存在
    if (componentOptions) {
      // 檢查是否緩存的模式匹配
      // check pattern
      // 獲取嵌套組件名稱
      const name: ?string = getComponentName(componentOptions)
      // 獲取傳入keep-alive組件的include和exclude屬性
      const { include, exclude } = this
      // 如果有included,且該組件不匹配included中資源
      // 或者有exclude。且該組件匹配exclude中的資源
      // 則返回虛擬節點,不繼續執行緩存
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      // 獲取keep-alive組件的cache和keys對象
      const { cache, keys } = this
      // 獲取嵌套組件虛擬節點的key
      const key: ?string = vnode.key == null
        // 同樣的構造函數可能被註冊爲不同的本地組件,所以cid不是判斷的充分條件
        // 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
      // 如果緩存對象裏有以key值存儲的組件資源
      if (cache[key]) {
        // 設置當前嵌套組件虛擬節點的componentInstance屬性
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        // 從keys中移除舊key,添加新key
        remove(keys, key)
        keys.push(key)
      } else {
        // 緩存中沒有該資源,則直接存儲資源,並存儲key值
        cache[key] = vnode
        keys.push(key)
        // 如果設置了最大緩存資源數,從最開始的序號開始刪除存儲資源
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      // 設置該資源虛擬節點的keepAlive標識
      vnode.data.keepAlive = true
    }
    // 返回虛擬節點或dom節點
    return vnode || (slot && slot[0])
  }
}

keep-alive 組件的實現也就這百來行代碼,分爲兩部分:第一部分是定義一些處理具體實現的函數,比如修剪緩存對象存儲資源的函數,匹配組件包含和過濾存儲的函數;第二部分是導出一份 keep-alive 組件的應用配置對象,仔細一下這跟我們在實際中使用的方式是一樣的,但這個組件具有已經定義好的特殊功能
,就是緩存嵌套在它之中的組件資源,實現持久活躍。

那麼實現原理是什麼,在代碼裏可以清楚得看到,這裏是利用轉換組件真實DOM節點爲虛擬節點將其存儲到 keep-alive 實例的 cache 對象中,另外也一併存儲了資源的 key 值方便查找,然後在渲染時檢測其是否符合緩存條件再進行渲染。keep-alive 的實現就是以上這樣簡單。


最初一瞥此段代碼時,不知所云。然而當開始逐步分析代碼之後,才發現原來只是沒有仔細去看,誤以爲很深奧,由此可見,任何不用心的行爲都不能直抵事物的本質,這是藉由探索這一小部分代碼而得到的教訓。因爲在實際中有使用過這個功能,所以體會更深,有時候難免會踩到一些坑,看了源碼的實現之後,發現原來是自己使用方式不對,所以瞭解所用輪子的實現還是很有必要的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章