1. 程式人生 > 程式設計 >從原始碼角度來回答keep-alive元件的快取原理

從原始碼角度來回答keep-alive元件的快取原理

今天開門見山地聊一下面試中被問到的一個問題:keep-alive元件的快取原理。

官方API介紹和用法

  • <keep-alive> 包裹動態元件時,會快取不活動的元件例項,而不是銷燬它們。
  • 和 <transition> 相似,<keep-alive> 是一個抽象元件:它自身不會渲染一個 DOM 元素,也不會出現在元件的父元件鏈中。
  • 當元件在 <keep-alive> 內被切換,它的 activated 和 deactivated 這兩個生命週期鉤子函式將會被對應執行。

官網的例子是 tab 切換儲存了使用者的操作,實際中還可能遇到從列表頁跳轉去了詳情頁,再跳轉回列表頁需要儲存使用者進行過的篩選操作,這就需要用到 <keep-alive>,這樣也能避免重新渲染,提高頁面效能。

用法及props的講解

// keep-alive元件搭配動態元件的用法,還要其他的用法參見官網
<keep-alive
 include="['componentNameA','componentNameB']"
 exclude="'componentNameC'"
 :max="10">
 <component :is="view"></component>
</keep-alive>

  • include - 字串或正則表示式或陣列,name匹配上的元件會被快取
  • exclude - 字串或正則表示式或陣列,name匹配上的元件都不會被快取
  • max - 字串或數字,快取元件例項的最大數,最久沒有被訪問的例項會被銷燬掉

注意:

  • <keep-alive> 只渲染其直系的一個元件,因此若在 <keep-alive> 中用 v-for,則其不會工作,若多條件判斷有多個符合條件也同理不工作。
  • include 和 exclude 匹配時,首先檢查元件的 name 選項,若 name 選項不可用,則匹配它的區域性註冊名稱 (即父元件 components 選項的鍵值)。匿名元件不能被匹配。
  • <keep-alive> 不會在函式式元件中正常工作,因為它們沒有快取例項。

原始碼解讀

先貼一張原始碼圖

從原始碼角度來回答keep-alive元件的快取原理

總共125行,收起來一看其實東西也比較少。前面是引入一些需要用到的方法,然後定義了一些 keep-alive 元件自己會用到的一些方法,最後就是向外暴露一個 name 為 keep-alive 的元件選項,這些選項除了 abstract 外,其他的我們都比較熟悉了,其中, render 函式就是快取原理最重要的部分,也能看出 keep-alive 元件是一個函式式元件。

// isRegExp函式判斷是不是正則表示式,remove移除陣列中的某一個成員
// getFirstComponentChild獲取VNode陣列的第一個有效元件
import { isRegExp,remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'
​
type VNodeCache = { [key: string]: ?VNode }; // 快取元件VNode的快取型別
​
// 通過元件的name或元件tag來獲取元件名(上面注意的第二點)
function getComponentName (opts: ?VNodeComponentOptions): ?string {
 return opts && (opts.Ctor.options.name || opts.tag)
}
​
// 判斷include或exclude跟元件的name是否匹配成功
function matches (pattern: string | RegExp | Array<string>,name: string): boolean {
 if (Array.isArray(pattern)) {
 return pattern.indexOf(name) > -1 // include或exclude是陣列的情況
 } else if (typeof pattern === 'string') {
 return pattern.split(',').indexOf(name) > -1 // include或exclude是字串的情況
 } else if (isRegExp(pattern)) {
 return pattern.test(name) // include或exclude是正則表示式的情況
 }
 return false // 都沒匹配上(上面注意的二三點)
}
​
// 銷燬快取
function pruneCache (keepAliveInstance: any,filter: Function) {
 const { cache,keys,_vnode } = keepAliveInstance // keep-alive元件例項
 for (const key in cache) {
 const cachedNode: ?VNode = cache[key] // 已經被快取的元件
 if (cachedNode) {
  const name: ?string = getComponentName(cachedNode.componentOptions)
  // 若name存在且不能跟include或exclude匹配上就銷燬這個已經快取的元件
  if (name && !filter(name)) {
  pruneCacheEntry(cache,key,_vnode)
  }
 }
 }
}
​
// 銷燬快取的入口
function pruneCacheEntry (
 cache: VNodeCache,key: string,keys: Array<string>,current?: VNode
) {
 const cached = cache[key] // 被快取過的元件
 // “已經被快取的元件是否繼續被快取” 有變動時
 // 若元件被快取命中過且當前元件不存在或快取命中元件的tag和當前元件的tag不相等
 if (cached && (!current || cached.tag !== current.tag)) {
 // 說明現在這個元件不需要被繼續快取,銷燬這個元件例項
 cached.componentInstance.$destroy()
 }
 cache[key] = null // 把快取中這個元件置為null
 remove(keys,key) // 把這個元件的key移除出keys陣列
}
​
// 示例型別
const patternTypes: Array<Function> = [String,RegExp,Array]
​
// 向外暴露keep-alive元件的一些選項
export default {
 name: 'keep-alive',// 元件名
 abstract: true,// keep-alive是抽象元件
​
 // 用keep-alive元件時傳入的三個props
 props: {
 include: patternTypes,exclude: patternTypes,max: [String,Number]
 },​
 created () {
 this.cache = Object.create(null) // 儲存需要快取的元件
 this.keys = [] // 儲存每個需要快取的元件的key,即對應this.cache物件中的鍵值
 },​
 // 銷燬keep-alive元件的時候,對快取中的每個元件執行銷燬
 destroyed () {
 for (const key in this.cache) {
  pruneCacheEntry(this.cache,this.keys)
 }
 },​
 // keep-alive元件掛載時監聽include和exclude的變化,條件滿足時就銷燬已快取的元件
 mounted () {
 this.$watch('include',val => {
  pruneCache(this,name => matches(val,name))
 })
 this.$watch('exclude',name => !matches(val,name))
 })
 },​
 // 重點來了
 render () {
 const slot = this.$slots.default // keep-alive元件的預設插槽
 const vnode: VNode = getFirstComponentChild(slot) // 獲取預設插槽的第一個有效元件
 // 如果vnode存在就取vnode的選項
 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
 if (componentOptions) {
  //獲取第一個有效元件的name
  const name: ?string = getComponentName(componentOptions)
  const { include,exclude } = this // props傳遞來的include和exclude
  if (
  // 若include存在且name不存在或name未匹配上
  (include && (!name || !matches(include,name))) ||
  // 若exclude存在且name存在或name匹配上
  (exclude && name && matches(exclude,name))
  ) {
  return vnode // 說明不用快取,直接返回這個元件進行渲染
  }
  
  // 匹配上就需要進行快取操作
  const { cache,keys } = this // keep-alive元件的快取元件和快取元件對應的key
  // 獲取第一個有效元件的key
  const key: ?string = vnode.key == null
  // 同一個建構函式可以註冊為不同的本地元件
  // 所以僅靠cid是不夠的,進行拼接一下
  ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
  : vnode.key
  // 如果這個元件命中快取
  if (cache[key]) {
  // 這個元件的例項用快取中的元件例項替換
  vnode.componentInstance = cache[key].componentInstance
  // 更新當前key在keys中的位置
  remove(keys,key) // 把當前key從keys中移除
  keys.push(key) // 再放到keys的末尾
  } else {
  // 如果沒有命中快取,就把這個元件加入快取中
  cache[key] = vnode
  keys.push(key) // 把這個元件的key放到keys的末尾
  // 如果快取中的元件個數超過傳入的max,銷燬快取中的LRU元件
  if (this.max && keys.length > parseInt(this.max)) {
   pruneCacheEntry(cache,keys[0],this._vnode)
  }
  }
​
  vnode.data.keepAlive = true // 設定這個元件的keepAlive屬性為true
 }
 // 若第一個有效的元件存在,但其componentOptions不存在,就返回這個元件進行渲染
 // 或若也不存在有效的第一個元件,但keep-alive元件的預設插槽存在,就返回預設插槽的第一個元件進行渲染
 return vnode || (slot && slot[0])
 }
}

補充:

上面關於刪除第一個舊快取元件和更新快取元件 key 的順序,其實是用到了LRU快取淘汰策略:
LRU全稱Least Recently Used,最近最少使用的意思,是一種記憶體管理演算法。
這種演算法基於一種假設:長期不用的資料,在未來被用到的機率也很小,因此,當資料所佔記憶體達到一定閾值,可以移除掉最近最少使用的。

總結

簡單總結為:

keep-alive 元件在渲染的時候,會根據傳入的 include 和 exclude 來匹配 keep-alive 包裹的命名元件,未匹配上就直接返回這個命名元件進行渲染,若匹配上就進行快取操作:若快取中已有這個元件,就替換其例項,並更新這個元件的 key 在 keys 中的位置;若快取中沒有這個元件,就把這個元件放入 keep-alive 元件的快取 cache 中,並把這個元件的 key 放入 keys 中,由於在 mounted 的時候有對 include 和 exclude 進行監聽,因此,後續這兩個屬性值發生變化時,會再次判斷是否滿足條件而進行元件銷燬。

到此這篇關於從原始碼角度來回答keep-alive元件的快取原理的文章就介紹到這了,更多相關keep-alive元件快取內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!