Vue - The Good Parts: keep-alive

前言

Vue 中有一個特別好用的組件 keep-alive 組件,咱們在不少場景下,都是能夠藉助於這個組件來提高咱們的產品體驗,基本上0成本實現緩存效果。用的最多的場景就是和路由搭配,緩存不怎麼更新的路由頁面。html

那這麼好用的功能,背後是怎麼實現的,有哪些能夠學習的,一塊兒來分析下。前端

正文分析

What

來自官網的介紹 cn.vuejs.org/v2/api/#kee…vue

image2021-6-23_17-18-35.png

能夠看到,他的使用,更多的是跟隨者動態組件一塊兒使用。最核心的就是緩存組件實例,以提高性能。在官網上也有一個tab切換的示例,就是他的一種使用場景 cn.vuejs.org/v2/guide/co…node

How

既然是一個內置組件,那麼它確定也就是一個按照組件來定義的,Vue 中核心的實如今 github.com/vuejs/vue/b… 這裏git

export default {
  // 組件名字
  name: 'keep-alive',
  // 抽象組件 這是一個沒有對外暴露的組件聲明屬性
  // 做用的話,就是不渲染DOM 也不會出如今父組件的children中
  abstract: true,
 
  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },
 
  methods: {
    cacheVNode() {
      const { cache, keys, vnodeToCache, keyToCache } = this
      // 存在須要緩存的節點
      if (vnodeToCache) {
        const { tag, componentInstance, componentOptions } = vnodeToCache
        // 緩存到 cache 對象中
        cache[keyToCache] = {
          name: getComponentName(componentOptions),
          tag,
          componentInstance,
        }
        keys.push(keyToCache)
        // 判斷是否超出了 max 最大緩存實例數
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          // 若是超出了 就銷燬超出的
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
        this.vnodeToCache = null
      }
    }
  },
 
  created () {
    // 初始化緩存對象
    this.cache = Object.create(null)
    this.keys = []
  },
 
  destroyed () {
    // 銷燬的時候 全部實例所有銷燬
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },
 
  mounted () {
    this.cacheVNode()
    // 若是這些有更新 同樣須要再次 check 一遍全部的緩存實例 是否應該緩存
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },
 
  updated () {
    // 更新鉤子 再次緩存
    this.cacheVNode()
  },
 
  render () {
    // 重點 render 的實現
    // 能夠得到組件內的默認內容 其實也就是 默認插槽內容
    const slot = this.$slots.default
    // 找到裏邊第一個組件節點
    const vnode: VNode = getFirstComponentChild(slot)
    // 經過 vnode 節點能夠得到組件配置項
    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
        // 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
        // 先刪除掉這個 key 而後在push 保證這個 key 是新鮮的 超限check的時候 有用
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      } else {
        // delay setting the cache until update
        // 設置好 vnodeToCache 當更新的時候 再去緩存 參考 updated 鉤子中的邏輯
        this.vnodeToCache = vnode
        this.keyToCache = key
      }
 
      vnode.data.keepAlive = true
    }
    // 返回的就是內部的節點
    return vnode || (slot && slot[0])
  }
}
複製代碼

上邊大概就是核心的一個流程:github

  • 默認進來,取得當前渲染的 vnode
  • 而後進入 mounted 鉤子,緩存上
  • 當有更新的時候,再次調用 render,設置 vnodeToCache
  • 到 updated 鉤子,再次緩存上
  • 下次若是命中緩存,直接用現有實例便可

你會發現要想很好的理解上述過程,要很好的理解 Vue 的生命週期,能夠參考 cn.vuejs.org/v2/guide/in… 圖以下:api

lifecycle.png

此外,還有不少關於 vnode 上的屬性,如:componentOptions、key、data、componentInstance、tag 等,以及相配合的在 Vue 中是如何識別和運用這些屬性的:如何不創新新的實例,如何觸發新的生命週期鉤子 activated deactivated 等,若是你對全部的邏輯細節比較感興趣,能夠參考黃老師的 ustbhuangyi.github.io/vue-analysi…數組

Why

咱們能夠理解爲 Vue 爲何提供了內置組件 keep-alive?緩存

在前言的部分,咱們也講了在實際場景中,仍是會遇到很多緩存組件的狀況,在遇到路由場景的時候更甚。markdown

那 Vue 的一個理念就是對開發者很友好,框架作了不少事情,使得開發者能夠專一於自身的邏輯開發工做,這也是爲何全球會有那麼多開發者鍾愛它的緣由之一。那從這個點出發,由於有這麼多的需求,因此 Vue 也就提供了這麼好用的內置組件也就不難理解了。

總結

咱們能夠看出,keep-alive 的組件實現並不複雜,所有文件也就 150 行上下,可是功能卻很強大,全部的功能參考 cn.vuejs.org/v2/api/#kee… 。那麼從這個組件上,咱們能夠學到些什麼東西,有什麼能夠借鑑的嗎?

Vue生命週期

組件定義雖然很少,可是倒是用到了 Vue 中絕大多數的生命週期鉤子,且是咱們也能常用到的:created、mounted、updated、destroyed。這也是咱們寫好組件的基石,正確理解並運用他們,知道他們的關係和過程,以及在對應的生命週期中適合作什麼樣的事情。

額外提一點,在 created 生命週期鉤子函數中,咱們看到了如何給實例定義一些非響應式的對象,能夠在 created 中直接 this.xxx = xxxx 的這種方式,而不是習慣性的把這些屬性放到 data() 中去定義(會變爲響應式對象,增長額外的開銷),這種技巧值得咱們學習和應用。

Vue手寫render

在這裏雖然十分簡單,直接返回了默認插槽內容的節點,可是咱們能夠從這個點出發,在一些特殊場景,仍是須要咱們去手寫 render 的,這部分也是須要咱們去熟練運用的,詳細的能夠參考官網 cn.vuejs.org/v2/guide/re… 關於 render 函數的使用以及createElement、數據對象。

keys設計

keys 是一個數組,咱們知道 keep-alive 還提供了一個能力:max 這個 prop,指定了最多能夠緩存多少組件實例,一旦這個數字達到了,在新實例被建立以前,已緩存組件中最久沒有被訪問的實例會被銷燬掉。注意這裏的關鍵:最久沒有被訪問的實例會被銷燬掉

這個是怎麼作到的,排序嗎?不用那麼麻煩,經過上邊的分析咱們知道Vue中採用了一個很巧妙的作法:

remove(keys, key)
keys.push(key)
複製代碼

簡單來講,將 keys 中如今對應的 key 刪除掉,而後把這個 key 再 push 到數組最後便可。

經過這種方式,就能夠保障了 keys 這個數組中最尾部的元素就是最新鮮的元素,最開始的元素就是最不新鮮的,在咱們的場景中就能夠對應爲:最近被訪問的實例。

// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
  pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
複製代碼

能夠看到實際代碼也是這樣的,當超出 max 的時候,就是銷燬的keys[0]對應的組件實例。

內存泄漏

在 destroyed 中,銷燬了全部的實例,以釋放在 cache 對象中緩存的對象們,這個就是對內存的管理,防止內存泄漏問題。這個是明面的內存銷燬邏輯,你們只要注意了就不會遇到內存泄漏的問題了。

可是這個不是絕對的,咱們看一下最新的這個PR github.com/vuejs/vue/p… ,咱們發現解決了兩個內存泄漏的 issue,直觀看起來是不該該存在內存泄漏的纔對。

這裏就驚醒咱們,要注意內存泄漏問題,他可能會是因爲咱們不經意之間寫的代碼所致使的。咱們須要作到怎麼利用開發者工具:

  • 如何知道內存泄漏了
  • 來定位&解決內存泄漏問題

其餘小Tips

  • 手工銷燬組件實例,調用實例的 $destroy()
  • 訪問 $slots.default 得到默認子節點們
  • vnode 上能夠訪問到 componentOptions以及 componentInstance,這倆仍是頗有用的

滴滴前端技術團隊的團隊號已經上線,咱們也同步了必定的招聘信息,咱們也會持續增長更多職位,有興趣的同窗能夠一塊兒聊聊。

相關文章
相關標籤/搜索