深刻keep-alive 組件實現

keep-alive 組件

用法

<!-- 基本 -->
<keep-alive>
  <component :is="view"></component>
</keep-alive>

<!-- 多個條件判斷的子組件 -->
<keep-alive>
  <comp-a v-if="a > 1"></comp-a>
  <comp-b v-else></comp-b>
</keep-alive>

<!-- 常見應用 -->
<keep-alive>
  <router-view></router-view>
</keep-alive>
複製代碼

keep-alive 要求同時只有一個子元素被渲染。javascript

props

include -- 逗號分隔字符串、正則表達式或一個數組。只有名稱匹配的組件會被緩存。
exclude -- 逗號分隔字符串、正則表達式或一個數組。任何名稱匹配的組件都不會被緩存。
max -- 最多能夠緩存多少組件實例。java

生命鉤子

keep-alive提供了兩個生命鉤子,分別是activateddeactivated
由於keep-alive會將組件保存在內存中,並不會銷燬以及從新建立,因此不會從新調用組件的created等方法,須要用activateddeactivated這兩個生命鉤子來得知當前組件是否處於活動狀態。node

keep-alive組件實現-源碼

咱們從源碼角度看一下keep-alive是如何實現組件的緩存的。正則表達式

const patternTypes: Array<Function> = [String, RegExp, Array]

export default {
  name: 'keep-alive',
  // 表示是抽象組件
  abstract: true, 

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },

  created () {
    // 用於緩存vnode對象
    this.cache = Object.create(null)
    this.keys = []
  },
  // destroyed鉤子中銷燬全部cache中的組件實例
  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },
  // 監聽include和exclude屬性
  mounted () {
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render () {
    // ...省略代碼(render函數後面詳細講解)
    return vnode || (slot && slot[0])
  }
}
複製代碼

created和destroy鉤子

首先看一下created鉤子中,緩存一個cache對象,用來緩存vnode節點。同時還緩存了一個keys數組,緩存的vnode節點的惟一標識。數組

destroyed鉤子在組件被銷燬的時候清除cache緩存中的全部組件實例。下面是銷燬組件的函數實現:1.把緩存中對應keyvnode的組件實例destroy;2.從cache中移除,並在keys數組中移除對應的key緩存

function pruneCacheEntry (cache, key, keys, current) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    // 判斷cache的vnode不是目前渲染的vnode,則銷燬
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}
複製代碼

render函數

接下來看一下組件中render函數的實現:作了詳細註釋函數

render () {
  // 獲取默認插槽
  const slot = this.$slots.default
  // 獲取第一個子組件(注意keepalive組件同時只能渲染一個子元素)
  const vnode: VNode = getFirstComponentChild(slot)
  // componentOptions中存儲了組件的配置項
  const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
  if (componentOptions) {
    // 獲取到組件的name,存在組件名則直接使用組件名,不然會使用tag
    const name: ?string = getComponentName(componentOptions)
    const { include, exclude } = this
    if (
      (include && (!name || !matches(include, name))) ||
      (exclude && name && matches(exclude, name))
    ) {
      // 若是配置了include可是組件名不匹配include 或者 配置了exclude且組件名匹配exclude,那麼就直接返回這個組件的vnode
      return vnode
    }

    const { cache, keys } = this
    // 獲取或生成表明組件的惟一標識key
    const key: ?string = vnode.key == null
      ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
      : vnode.key
    if (cache[key]) {
      // cache緩存中存在這個vnode,那麼直接將緩存的vnode的componentInstance(組件實例)覆蓋到目前的vnode上面
      vnode.componentInstance = cache[key].componentInstance
      // 更新keys數組,讓最新渲染的vnodekey推到尾部
      remove(keys, key)
      keys.push(key)
    } else {
      // 若是沒有緩存過,則將vnode緩存到cache中
      cache[key] = vnode
      keys.push(key)
      // 若是緩存的組件實例數量大於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])
}
複製代碼

首先獲取到它的默認插槽,而後再獲取到它的第一個子組件,獲取該組件的name(存在組件名則直接使用組件名,不然會使用tag)。接下來會將這個name經過includeexclude屬性進行匹配,匹配不成功(說明不須要進行緩存)則不進行任何操做直接返回vnodeui

檢測includeexclude屬性匹配的函數很簡單:this

// 判斷name是否匹配pattern
function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
  if (Array.isArray(pattern)) {
    return pattern.indexOf(name) > -1
  } else if (typeof pattern === 'string') {
    return pattern.split(',').indexOf(name) > -1
  } else if (isRegExp(pattern)) {
    return pattern.test(name)
  }
  /* istanbul ignore next */
  return false
}
複製代碼

includeexclude屬性支持數組、支持字符串如"a,b,c"這樣組件名以逗號隔開的狀況以及正則表達式。matches經過這三種方式分別檢測是否匹配當前組件。spa

接着,根據keythis.cache中查找,若是存在則說明以前已經緩存過了,直接將緩存的vnodecomponentInstance(組件實例)覆蓋到目前的vnode上面。不然將vnode存儲在cache中。

最後返回vnode(有緩存時該vnodecomponentInstance已經被替換成緩存中的了)。

mounted鉤子

咱們注意到mounted鉤子中,咱們對exclude屬性和include屬性作了監聽。

mounted () {
  this.$watch('include', val => {
    pruneCache(this, name => matches(val, name))
  })
  this.$watch('exclude', val => {
    pruneCache(this, name => !matches(val, name))
  })
}
複製代碼

也就是說,咱們會監聽這兩個屬性的變化,改變的時候修改cache緩存中的緩存數據。其實會對cache作遍歷,發現緩存的節點名稱和新的規則沒有匹配上的時候,就把這個緩存節點從緩存中摘除。咱們看看pruneCache函數的實現:

function pruneCache (keepAliveInstance: any, filter: Function) {
  // _vnode:表示目前組件的渲染節點
  const { cache, keys, _vnode } = keepAliveInstance
  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)
      }
    }
  }
}
複製代碼

遍歷cache緩存,name不符合filter條件的時候則調用pruneCacheEntry方法銷燬vnode對應的組件實例(Vue實例),並從cache中移除。

總結

keep-alive組件是一個抽象組件,而且它的緩存是基於VNode節點的緩存,它的實現是經過自定義render函數而且利用了插槽。並且會隨時監聽include和exclude屬性的變化作緩存數據的變化。

相關文章
相關標籤/搜索