Vue.js設計與實現18-KeepAlive的原理與實現

語言: CN / TW / HK

1.寫在前面

前面文章介紹了Vue.js通過渲染器實現元件化的能力,介紹了有狀態元件和無狀態元件的構造與實現,還有非同步元件對於框架的意義。本文將主要介紹Vue.js的重要內建元件和模組--KeepAlive元件。

2.KeepAlive元件

KeepAlive字面意思理解就是保持鮮活,就是建立持久連線的意思,可以避免元件或連線頻繁地建立和銷燬。

<template>
  <KeepAlive>
    <Tab v-if="currentTab === 1"/>
    <Tab v-if="currentTab === 2"/>
    <Tab v-if="currentTab === 3"/>
  </KeepAlive>
</template>

在上面程式碼中,會根據currentTab變數的值頻繁切換Tab元件,會導致不停地解除安裝和重建對應的Tab元件,為了避免因此產生的效能開銷,可以使用KeepAlive元件保持元件的鮮活。那麼KeepAlive元件是如何保持元件的鮮活的,其實就會對元件進行快取管理,避免元件頻繁的解除安裝和重建。

其實,就是通過一個隱藏的元件快取容器,將元件需要的時候將其放到容器裡,在需要重建使用的時候將其取出,這樣對於使用者感知是進行了“解除安裝”和“重建”元件。在元件搬運到快取容器和搬出,就是對應元件的生命週期activated和deactivated。

3.元件的失活和啟用

那麼,應該如何實現元件的快取管理呢?

const KeepAlive = {
  // keepAlive元件的識別符號
  _isKeepAlive:true,
  setup(props,{slots}){
    //快取容器
    const cache = new Map();
    const instance = currentInstance;
    const { move, createElement } = instance.keepAliveCtx;
    
    //隱藏容器
    const storageContainer = createElement("div");
    
    instance._deActivate = (vnode)=>{
      move(vnode, storageContainer)
    };
    
    instance._activate = (vnode, container, anchor)=>{
      move(vnode, container, anchor)
    };
    
    return ()=>{
      let rawNode = slots.default();
      // 非元件的虛擬節點無法被keepAlive
      if(typeof rawNode.type !== "object"){
        return rawNode;
      }
      
      //在掛在時先獲取快取的元件vnode
      const cacheVNode = cache.get(rawNode.type);
      if(cacheVNode){
        rawVNode.component = cacheVNode.component;
        rawVNode.keptAlive = true;
      }else{
        cache.set(rawVNode.type, rawVNode);
      }
      
      rawVNode.shouldKeepAlive = true;
      rawVNode.keepAliveInstance = instance;
      
      // 渲染元件vnode
      return rawVNode
    }
  }
}

在上面程式碼中,KeepAlive元件本身不會渲染額外的內容,渲染函式只返回被KeepAlive的元件,被稱為“內部元件”,KeepAlive會在“內部元件”的Vnode物件上新增標記屬性,便於渲染器執行特定邏輯。

  • shouldKeepAlive屬性會被新增到“內部元件”的vnode物件上,當渲染器解除安裝“內部元件”時,可以通過檢查屬性得知“內部元件”是否需要被KeepAlive。
  • keepAliveInstance:內部元件的vnode物件會持有keepAlive元件例項,在unmount函式中通過keepAliveInstance訪問_deactivate函式。
  • keptAlive:內部元件已被快取則新增keptAlive標記,判斷內部元件重新渲染時是否需要重新掛載還是啟用。
function unmount(vnode){
  if(vnode.type === Fragment){
    vnode.children.forEach(comp=>unmount(comp));
    return;
  }else if(typeof vnode.type === "object"){
    if(vnode.shouldKeepAlive){
      vnode.keepAliveInstance._deactivate(vnode);
    }else{
      unmount(vnode.component.subTree);
    }
    return
  }
  const parent = vnode.el.parentVNode;
  if(parent){
    parent.removeChild(vnode.el);
  }
}

元件失活的本質是將元件所渲染的內容移動到隱藏容器中,啟用的本質是將元件所要渲染的內容從隱藏容器中搬運回原來的容器。

const { move, createElement } = instance.keepAliveCtx;

instance._deActivate = (vnode)=>{
  move(vnode, storageContainer);
}

instance._activate = (vnode, container, anchor)=>{
  move(vnode, container, anchor);
}

4.include和exclude

我們看到上面的程式碼會對元件所有的"內部元件"進行快取,但是使用者又想自定義快取規則,只對特定元件進行快取,對此KeepAlive元件需要支援兩個props:include和exclude。

  • include:用於顯式配置應被快取的元件
  • exclude:用於顯式配置不應該被快取的元件
const cache = new Map();
const keepAlive = {
  __isKeepAlive: true,
  props:{
    include: RegExp,
    exclude: RegExp
  },
  setup(props, {slots}){
    //...
    return ()=>{
      let rawVNode = slots.default();
      if(typeof rawVNode.type !== "object"){
        return rawVNode;
      }
      const name = rawVNode.type.name;
      if(name && (
        (props.include && !props.include.test(name)) ||
        (props.exclude && props.include.test(name))
      )){
        //直接渲染內部元件,不對其進行快取操作
        return rawVNode
      }
    }
  }
}

上面程式碼中,為了簡便闡述問題進行設定正則型別的值,在KeepAlive元件被掛載時,會根據"內部元件"的名稱進行匹配,根據匹配結果判斷是否要對元件進行快取。

5.快取管理

在前面小節中使用Map物件實現對元件的快取,Map的鍵值對分別對應的是元件vnode.type屬性值和描述該元件的vnode物件。因為用於描述元件的vnode物件存在對元件例項的引用,對此快取用於描述元件的vnode物件,等價於快取了元件例項。

前面介紹的keepAlive元件實現快取的處理邏輯是:

  • 快取存在時繼承元件例項,將描述元件的vnode物件標記為keptAlive,渲染器不會重新建立新的元件例項
  • 快取不存在時,則設定快取

但是,如果快取不存在時,那麼總是會設定新的快取,這樣導致快取不斷增加,會佔用大量記憶體。對此,我們需要設定個記憶體閾值,在快取數量超過指定閾值時需要對快取進行修剪,在Vue.js中使用的是"最新一次訪問"策略。

"最新一次訪問"策略本質上就是通過設定當前訪問或渲染的元件作為最新一次渲染的元件,並且該元件在修剪過程中始終是安全的,即不會被修剪。

快取例項中需要滿足固定的格式:

const _cache = new Map();
const cache: KeepAliveCache = {
  get(key){
    _cache.get(key);
  },
  set(key, value){
    _cache.set(key, value);
  },
  delete(key){
    _cache.delete(key);
  },
  forEach(fn){
    _cache.forEach(fn);
  }
}

6.寫在最後

本文簡單介紹了Vue.js中KeepAlive元件的設計與實現原理,可以實現對元件的快取,避免元件例項不斷地銷燬和重建。KeepAlive元件解除安裝時渲染器並不會真實地把它進行解除安裝,而是將該元件搬運到另外一個隱藏容器裡,從而使得元件能夠維持當前狀態。在KeepAlive元件掛載時,渲染器將其從隱藏容器中搬運到原容器中。此外,我們還討論了KeepAlive元件的include和exclude自定義快取,以及快取管理。