Vue源碼解析篇 (二)keep-alive源碼解析

keep-alive是Vue.js的一個內置組件。它能夠不活動的組件實例保存在內存中,我們來探究一下它的源碼實現。

首先回顧下使用方法

舉個栗子

<keep-alive>
    <component-a v-if="isShow"></component-a>
    <component-b v-else></component-b>
</keep-alive>
<button @click="test=handleClick">請點擊</button>
export default {
    data () {
        return {
            isShow: true
        }
    },
    methods: {
        handleClick () {
            this.isShow = !this.isShow;
        }
    }
}

在點擊按鈕時,兩個組件會發生切換,但是這時候這兩個組件的狀態會被緩存起來,比如:組件中都有一個input標籤,那麼input標籤中的內容不會因爲組件的切換而消失。

屬性支持

keep-alive組件提供了includeexclude兩個屬性來允許組件有條件地進行緩存,二者都可以用逗號分隔字符串、正則表達式或一個數組來表示。

舉個例子:

  • 緩存name爲a的組件。
<keep-alive include="a">
  <component></component>
</keep-alive>
  • 排除緩存name爲a的組件。
<keep-alive exclude="a">
  <component></component>
</keep-alive>

當然 props 還定義了 max,該配置允許我們指定緩存大小。

keep-alive 源碼實現

說完了keep-alive組件的使用,我們從源碼角度看一下keep-alive組件究竟是如何實現組件的緩存的呢?

創建和銷燬階段

首先看看 keep-alive 的創建和銷燬階段做了什麼事情:

created () {
    /* 緩存對象 */
    this.cache = Object.create(null)
},
destroyed () {
    for (const key in this.cache) {
        pruneCacheEntry(this.cache[key])
    }
},
  • keep-alive 的創建階段: created鉤子會創建一個cache對象,用來保存vnode節點。
  • 在銷燬階段:destroyed 鉤子則會調用pruneCacheEntry方法清除cache緩存中的所有組件實例。

pruneCacheEntry 方法的源碼實現

/* 銷燬vnode對應的組件實例(Vue實例) */
function pruneCacheEntry (vnode: ?VNode) {
  if (vnode) {
    vnode.componentInstance.$destroy()
  }
}

因爲keep-alive會將組件保存在內存中,並不會銷燬以及重新創建,所以不會重新調用組件的created等方法,因此keep-alive提供了兩個生命鉤子,分別是activateddeactivated。用這兩個生命鉤子得知當前組件是否處於活動狀態。(稍後會看源碼如何實現)

渲染階段
render () {
    /* 得到slot插槽中的第一個組件 */
    const vnode: VNode = getFirstComponentChild(this.$slots.default)

    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
        // 獲取組件名稱,優先獲取組件的name字段,否則是組件的tag
        const name: ?string = getComponentName(componentOptions)

        // 不需要緩存,則返回 vnode
        if (name && (
        (this.include && !matches(this.include, name)) ||
        (this.exclude && matches(this.exclude, name))
        )) {
            return vnode
        }
        const key: ?string = vnode.key == null
            ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
            : vnode.key
        if (this.cache[key]) {
           // 有緩存則取緩存的組件實例
            vnode.componentInstance = this.cache[key].componentInstance
        } else {
            // 無緩存則創建緩存
            this.cache[key] = vnode
            
            // 創建緩存時
            // 如果配置了 max 並且緩存的長度超過了 this.max
            // 則從緩存中刪除第一個
            if (this.max && keys.length > parseInt(this.max)) {
              pruneCacheEntry(this.cache, keys[0], keys, this._vnode)
            }
        }
        // keepAlive標記
        vnode.data.keepAlive = true
    }
    return vnode
}

render 做了以下事情:

  1. 通過getFirstComponentChild獲取第一個子組件,獲取該組件的name(存在組件名則直接使用組件名,否則會使用tag)
  2. 將name通過include與exclude屬性進行匹配,匹配不成功(說明不需要緩存)則直接返回vnode
  3. 匹配成功則嘗試獲取緩存的組件實例
  4. 若沒有緩存該組件,則緩存該組件
  5. 緩存超過最大值會刪掉第一個緩存

name 匹配的方法(校驗是逗號分隔的字符串還是正則)

/* 檢測name是否匹配 */
function matches (pattern: string | RegExp, name: string): boolean {
  if (typeof pattern === 'string') {
    /* 字符串情況,如a,b,c */
    return pattern.split(',').indexOf(name) > -1
  } else if (isRegExp(pattern)) {
    /* 正則 */
    return pattern.test(name)
  }
  /* istanbul ignore next */
  return false
}

如果在中途有對 includeexclude 進行修改該怎麼辦呢?

作者通過 watch 來監聽 includeexclude,在其改變時調用 pruneCache 以修改 cache 緩存中的緩存數據。

watch: {
    /* 監視include以及exclude,在被修改的時候對cache進行修正 */
    include (val: string | RegExp) {
        pruneCache(this.cache, this._vnode, name => matches(val, name))
    },
    exclude (val: string | RegExp) {
        pruneCache(this.cache, this._vnode, name => !matches(val, name))
    }
},

那麼 pruneCache 做了什麼?

// 修補 cache
function pruneCache (cache: VNodeCache, current: VNode, filter: Function) {
  for (const key in cache) {
    // 嘗試獲取 cache中的vnode
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      const name: ?string = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) { // 重新篩選組件
        if (cachedNode !== current) { // 不在當前 _vnode 中
          pruneCacheEntry(cachedNode) // 調用組件實例的 銷燬方法
        }
        cache[key] = null // 移除該緩存
      }
    }
  }
} 

pruneCache方法 遍歷cache中的所有項,如果不符合規則則會銷燬該節點並移除該緩存

進階

再回顧下源碼,在 src/core/components/keep-alive.js

export default {
  name: 'keep-alive,
  abstract: true,

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

  created () {
    this.cache = Object.create(null)
    this.keys = []
  },

  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

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

  render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      const key: ?string = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        remove(keys, key)
        keys.push(key)
      } else {
        cache[key] = vnode
        keys.push(key)
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}

現在不加註釋也應該大部分都能看懂了?

順便提下 abstract 這個屬性,若 abstracttrue,則表示組件是一個抽象組件,不會被渲染到真實DOM中,也不會出現在父組件鏈中。

那麼爲什麼在組件有緩存的時候不會再次執行組件的 createdmounted 等鉤子函數呢?

const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    // 進入這段邏輯
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      const mountedNode: any = vnode 
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
  // ...
}

看上面了代碼, 滿足 vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive 的邏輯就不會執行$mount的操作,而是執行prepatch

那麼 prepatch 究竟做了什麼?

  // 不重要內容都省略...
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    // 會執行這個方法
    updateChildComponent(//...)
  },
  // ...

其中主要是執行了 updateChildComponent 函數。

function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  const hasChildren = !!(
    renderChildren ||          
    vm.$options._renderChildren ||
    parentVnode.data.scopedSlots || 
    vm.$scopedSlots !== emptyObject 
  )

  // ...
  if (hasChildren) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }
}

keep-alive 組件本質上是通過 slot 實現的,所以它執行 prepatch 的時候,hasChildren = true,會觸發組件的 $forceUpdate 邏輯,也就是重新執行 keep-alive 的 render 方法

然鵝,根據上面講的 render 方法源碼,就會去找緩存咯。

那麼,<keep-alive> 的實現原理就介紹完了

最後

  1. 原創不易點個讚唄
  2. 歡迎關注公衆號「前端進階課」認真學前端,一起進階。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章