完全搞懂Vue中keep-alive的魔法(上)

上一節最後稍微提到了Vue內置組件的相關內容,從這一節開始,將會對某個具體的內置組件進行分析。首先是keep-alive,它是咱們平常開發中常用的組件,咱們在不一樣組件間切換時,常常要求保持組件的狀態,以免重複渲染組件形成的性能損耗,而keep-alive常常和上一節介紹的動態組件結合起來使用。因爲內容過多,keep-alive的源碼分析將分爲上下兩部分,這一節主要圍繞keep-alive的首次渲染展開。vue

13.1 基本用法

keep-alive的使用只須要在動態組件的最外層添加標籤便可。node

<div id="app">
    <button @click="changeTabs('child1')">child1</button>
    <button @click="changeTabs('child2')">child2</button>
    <keep-alive>
        <component :is="chooseTabs">
        </component>
    </keep-alive>
</div>
var child1 = {
    template: '<div><button @click="add">add</button><p>{{num}}</p></div>',
    data() {
        return {
            num: 1
        }
    },
    methods: {
        add() {
            this.num++
        }
    },
}
var child2 = {
    template: '<div>child2</div>'
}
var vm = new Vue({
    el: '#app',
    components: {
        child1,
        child2,
    },
    data() {
        return {
            chooseTabs: 'child1',
        }
    },
    methods: {
        changeTabs(tab) {
            this.chooseTabs = tab;
        }
    }
})
複製代碼

簡單的結果以下,動態組件在child1,child2之間來回切換,當第二次切到child1時,child1保留着原來的數據狀態,num = 5react

13.2 從模板編譯到生成vnode

按照以往分析的經驗,咱們會從模板的解析開始提及,第一個疑問即是:內置組件和普通組件在編譯過程有區別嗎?答案是沒有的,不論是內置的仍是用戶定義組件,本質上組件在模板編譯成render函數的處理方式是一致的,這裏的細節不展開分析,有疑惑的能夠參考前幾節的原理分析。最終針對keep-aliverender函數的結果以下:算法

with(this){···_c('keep-alive',{attrs:{"include":"child2"}},[_c(chooseTabs,{tag:"component"})],1)}api

有了render函數,接下來從子開始到父會執行生成Vnode對象的過程,_c('keep-alive'···)的處理,會執行createElement生成組件Vnode,其中因爲keep-alive是組件,因此會調用createComponent函數去建立子組件Vnode,createComponent以前也有分析過,這個環節和建立普通組件Vnode不一樣之處在於,keep-aliveVnode會剔除多餘的屬性內容,因爲keep-alive除了slot屬性以外,其餘屬性在組件內部並無意義,例如class樣式,<keep-alive clas="test"></keep-alive>等,因此在Vnode層剔除掉多餘的屬性是有意義的。而<keep-alive slot="test">的寫法在2.6以上的版本也已經被廢棄。(其中abstract做爲抽象組件的標誌,以及其做用咱們後面會講到)數組

// 建立子組件Vnode過程
function createComponent(Ctordata,context,children,tag) {
    // abstract是內置組件(抽象組件)的標誌
    if (isTrue(Ctor.options.abstract)) {
        // 只保留slot屬性,其餘標籤屬性都被移除,在vnode對象上再也不存在
        var slot = data.slot;
        data = {};
        if (slot) {
            data.slot = slot;
        }
    }
}
複製代碼

13.3 初次渲染

keep-alive之因此特別,是由於它不會重複渲染相同的組件,只會利用初次渲染保留的緩存去更新節點。因此爲了全面瞭解它的實現原理,咱們須要從keep-alive的首次渲染開始提及。緩存

13.3.1 流程圖

爲了理清楚流程,我大體畫了一個流程圖,流程圖大體覆蓋了初始渲染keep-alive所執行的過程,接下來會照着這個過程進行源碼分析。bash

和渲染普通組件相同的是,Vue會拿到前面生成的Vnode對象執行真實節點建立的過程,也就是熟悉的patch過程,patch執行階段會調用createElm建立真實dom,在建立節點途中,keep-alivevnode對象會被認定是一個組件Vnode,所以針對組件Vnode又會執行createComponent函數,它會對keep-alive組件進行初始化和實例化。app

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
      var i = vnode.data;
      if (isDef(i)) {
        // isReactivated用來判斷組件是否緩存。
        var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
        if (isDef(i = i.hook) && isDef(i = i.init)) {
            // 執行組件初始化的內部鉤子 init
          i(vnode, false /* hydrating */);
        }
        if (isDef(vnode.componentInstance)) {
          // 其中一個做用是保留真實dom到vnode中
          initComponent(vnode, insertedVnodeQueue);
          insert(parentElm, vnode.elm, refElm);
          if (isTrue(isReactivated)) {
            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
          }
          return true
        }
      }
    }
複製代碼

keep-alive組件會先調用內部鉤子init方法進行初始化操做,咱們先看看init過程作了什麼操做。dom

// 組件內部鉤子
var componentVNodeHooks = {
    init: function init (vnode, hydrating) {
      if (
        vnode.componentInstance &&
        !vnode.componentInstance._isDestroyed &&
        vnode.data.keepAlive
      ) {
        // kept-alive components, treat as a patch
        var mountedNode = vnode; // work around flow
        componentVNodeHooks.prepatch(mountedNode, mountedNode);
      } else {
          // 將組件實例賦值給vnode的componentInstance屬性
        var child = vnode.componentInstance = createComponentInstanceForVnode(
          vnode,
          activeInstance
        );
        child.$mount(hydrating ? vnode.elm : undefined, hydrating);
      }
    },
    // 後面分析
    prepatch: function() {}
}
複製代碼

第一次執行,很明顯組件vnode沒有componentInstance屬性,vnode.data.keepAlive也沒有值,因此會調用createComponentInstanceForVnode方法進行組件實例化並將組件實例賦值給vnodecomponentInstance屬性, 最終執行組件實例的$mount方法進行實例掛載。

createComponentInstanceForVnode就是組件實例化的過程,而組件實例化從系列的第一篇就開始說了,無非就是一系列選項合併,初始化事件,生命週期等初始化操做。

function createComponentInstanceForVnode (vnode, parent) {
    var options = {
      _isComponent: true,
      _parentVnode: vnode,
      parent: parent
    };
    // 內聯模板的處理,忽略這部分代碼
    ···
    // 執行vue子組件實例化
    return new vnode.componentOptions.Ctor(options)
  }
複製代碼
13.3.2 內置組件選項

咱們在使用組件的時候常常利用對象的形式定義組件選項,包括data,method,computed等,並在父組件或根組件中註冊。keep-alive一樣遵循這個道理,內置兩字也說明了keep-alive是在Vue源碼中內置好的選項配置,而且也已經註冊到全局,這一部分的源碼能夠參考深刻剖析Vue源碼 - Vue動態組件的概念,你會亂嗎?小節末尾對內置組件構造器和註冊過程的介紹。這一部分咱們重點關注一下keep-alive的具體選項。

// keepalive組件選項
  var KeepAlive = {
    name: 'keep-alive',
    // 抽象組件的標誌
    abstract: true,
    // keep-alive容許使用的props
    props: {
      include: patternTypes,
      exclude: patternTypes,
      max: [String, Number]
    },

    created: function created () {
      // 緩存組件vnode
      this.cache = Object.create(null);
      // 緩存組件名
      this.keys = [];
    },

    destroyed: function destroyed () {
      for (var key in this.cache) {
        pruneCacheEntry(this.cache, key, this.keys);
      }
    },

    mounted: function mounted () {
      var this$1 = this;
      // 動態include和exclude
      // 對include exclue的監聽
      this.$watch('include', function (val) {
        pruneCache(this$1, function (name) { return matches(val, name); });
      });
      this.$watch('exclude', function (val) {
        pruneCache(this$1, function (name) { return !matches(val, name); });
      });
    },
    // keep-alive的渲染函數
    render: function render () {
      // 拿到keep-alive下插槽的值
      var slot = this.$slots.default;
      // 第一個vnode節點
      var vnode = getFirstComponentChild(slot);
      // 拿到第一個組件實例
      var componentOptions = vnode && vnode.componentOptions;
      // keep-alive的第一個子組件實例存在
      if (componentOptions) {
        // check pattern
        //拿到第一個vnode節點的name
        var name = getComponentName(componentOptions);
        var ref = this;
        var include = ref.include;
        var exclude = ref.exclude;
        // 經過判斷子組件是否知足緩存匹配
        if (
          // not included
          (include && (!name || !matches(include, name))) ||
          // excluded
          (exclude && name && matches(exclude, name))
        ) {
          return vnode
        }

        var ref$1 = this;
        var cache = ref$1.cache;
        var keys = ref$1.keys;
        var key = vnode.key == null
          ? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
          : vnode.key;
          // 再次命中緩存
        if (cache[key]) {
          vnode.componentInstance = cache[key].componentInstance;
          // make current key freshest
          remove(keys, key);
          keys.push(key);
        } else {
        // 初次渲染時,將vnode緩存
          cache[key] = vnode;
          keys.push(key);
          // prune oldest entry
          if (this.max && keys.length > parseInt(this.max)) {
            pruneCacheEntry(cache, keys[0], keys, this._vnode);
          }
        }
        // 爲緩存組件打上標誌
        vnode.data.keepAlive = true;
      }
      // 將渲染的vnode返回
      return vnode || (slot && slot[0])
    }
  };
複製代碼

keep-alive選項跟咱們平時寫的組件選項仍是基本相似的,惟一的不一樣是keep-ailve組件沒有用template而是使用render函數。keep-alive本質上只是存緩存和拿緩存的過程,並無實際的節點渲染,因此使用render處理是最優的選擇。

13.3.3 緩存vnode

仍是先回到流程圖的分析。上面說到keep-alive在執行組件實例化以後會進行組件的掛載。而掛載$mount又回到vm._render(),vm._update()的過程。因爲keep-alive擁有render函數,因此咱們能夠直接將焦點放在render函數的實現上。

    1. 首先是獲取keep-alive下插槽的內容,也就是keep-alive須要渲染的子組件,例子中是chil1 Vnode對象,源碼中對應getFirstComponentChild函數
function getFirstComponentChild (children) {
    if (Array.isArray(children)) {
      for (var i = 0; i < children.length; i++) {
        var c = children[i];
        // 組件實例存在,則返回,理論上返回第一個組件vnode
        if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
          return c
        }
      }
    }
  }

複製代碼
    1. 判斷組件知足緩存的匹配條件,在keep-alive組件的使用過程當中,Vue源碼容許咱們是用include, exclude來定義匹配條件,include規定了只有名稱匹配的組件纔會被緩存,exclude規定了任何名稱匹配的組件都不會被緩存。更者,咱們可使用max來限制能夠緩存多少匹配實例,而爲何要作數量的限制呢?咱們後文會提到。

拿到子組件的實例後,咱們須要先進行是否知足匹配條件的判斷,其中匹配的規則容許使用數組,字符串,正則的形式。

var include = ref.include;
var exclude = ref.exclude;
// 經過判斷子組件是否知足緩存匹配
if (
    // not included
    (include && (!name || !matches(include, name))) ||
    // excluded
    (exclude && name && matches(exclude, name))
) {
    return vnode
}

// matches
function matches (pattern, name) {
    // 容許使用數組['child1', 'child2']
    if (Array.isArray(pattern)) {
        return pattern.indexOf(name) > -1
    } else if (typeof pattern === 'string') {
        // 容許使用字符串 child1,child2
        return pattern.split(',').indexOf(name) > -1
    } else if (isRegExp(pattern)) {
        // 容許使用正則 /^child{1,2}$/g
        return pattern.test(name)
    }
    /* istanbul ignore next */
    return false
}
複製代碼

若是組件不知足緩存的要求,則直接返回組件的vnode,不作任何處理,此時組件會進入正常的掛載環節。

    1. render函數執行的關鍵一步是緩存vnode,因爲是第一次執行render函數,選項中的cachekeys數據都沒有值,其中cache是一個空對象,咱們將用它來緩存{ name: vnode }枚舉,而keys咱們用來緩存組件名。 所以咱們在第一次渲染keep-alive時,會將須要渲染的子組件vnode進行緩存。
cache[key] = vnode;
    keys.push(key);
複製代碼
    1. 將已經緩存的vnode打上標記, 並將子組件的Vnode返回。 vnode.data.keepAlive = true
13.3.4 真實節點的保存

咱們再回到createComponent的邏輯,以前提到createComponent會先執行keep-alive組件的初始化流程,也包括了子組件的掛載。而且咱們經過componentInstance拿到了keep-alive組件的實例,而接下來重要的一步是將真實的dom保存再vnode

function createComponent(vnode, insertedVnodeQueue) {
    ···
    if (isDef(vnode.componentInstance)) {
        // 其中一個做用是保留真實dom到vnode中
        initComponent(vnode, insertedVnodeQueue);
        // 將真實節點添加到父節點中
        insert(parentElm, vnode.elm, refElm);
        if (isTrue(isReactivated)) {
            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
        }
        return true
    }
}
複製代碼

insert的源碼不列舉出來,它只是簡單的調用操做domapi,將子節點插入到父節點中,咱們能夠重點看看initComponent關鍵步驟的邏輯。

function initComponent() {
    ···
    // vnode保留真實節點
    vnode.elm = vnode.componentInstance.$el;
    ···
}
複製代碼

所以,咱們很清晰的回到以前遺留下來的問題,爲何keep-alive須要一個max來限制緩存組件的數量。緣由就是keep-alive緩存的組件數據除了包括vnode這一描述對象外,還保留着真實的dom節點,而咱們知道真實節點對象是龐大的,因此大量保留緩存組件是耗費性能的。所以咱們須要嚴格控制緩存的組件數量,而在緩存策略上也須要作優化,這點咱們在下一篇文章也繼續提到。

因爲isReactivatedfalse,reactivateComponent函數也不會執行。至此keep-alive的初次渲染流程分析完畢。

若是忽略步驟的分析,只對初次渲染流程作一個總結:內置的keep-alive組件,讓子組件在第一次渲染的時候將vnode和真實的elm進行了緩存。

13.4 抽象組件

這一節的最後順便提一下上文提到的抽象組件的概念。Vue提供的內置組件都有一個描述組件類型的選項,這個選項就是{ astract: true },它代表了該組件是抽象組件。什麼是抽象組件,爲何要有這一類型的區別呢?我以爲歸根究底有兩個方面的緣由。

    1. 抽象組件沒有真實的節點,它在組件渲染階段不會去解析渲染成真實的dom節點,而只是做爲中間的數據過渡層處理,在keep-alive中是對組件緩存的處理。
    1. 在咱們介紹組件初始化的時候曾經說到父子組件會顯式的創建一層關係,這層關係奠基了父子組件之間通訊的基礎。咱們能夠再次回顧一下initLifecycle的代碼。
Vue.prototype._init = function() {
    ···
    var vm = this;
    initLifecycle(vm)
}

function initLifecycle (vm) {
    var options = vm.$options;
    
    var parent = options.parent;
    if (parent && !options.abstract) {
        // 若是有abstract屬性,一直往上層尋找,直到不是抽象組件
      while (parent.$options.abstract && parent.$parent) {
        parent = parent.$parent;
      }
      parent.$children.push(vm);
    }
    ···
  }
複製代碼

子組件在註冊階段會把父實例掛載到自身選項的parent屬性上,在initLifecycle過程當中,會反向拿到parent上的父組件vnode,併爲其$children屬性添加該子組件vnode,若是在反向找父組件的過程當中,父組件擁有abstract屬性,便可斷定該組件爲抽象組件,此時利用parent的鏈條往上尋找,直到組件不是抽象組件爲止。initLifecycle的處理,讓每一個組件都能找到上層的父組件以及下層的子組件,使得組件之間造成一個緊密的關係樹。

有了第一次的緩存處理,當第二次渲染組件時,keep-alive又會有哪些魔法的存在呢,以前留下的緩存優化又是什麼?這些都會在下一小節一一解開。


相關文章
相關標籤/搜索