11. 探究Vue的keep-alive

keep-alive的運行過程

keep-alive是個抽象組件(或稱爲功能型組件),實際上不會被渲染在DOM樹中。它的做用是在內存中緩存組件(不讓組件銷燬),等到下次再渲染的時候,還會保持其中的全部狀態,而且會觸發activated鉤子函數。通常與或者動態組件配合使用,本篇以動態組件爲例: keep-alive的模板vue

<div id="app"><keep-alive><component :is="view"></component></keep-alive><button @click="changeView">切換</button></div>  
複製代碼

子組件模板分別爲node

Vue.component('view1', {
    template: '<div>view component1</div>'
})
Vue.component('view2', {
    template: '<div>view component2</div>'
})
複製代碼

react

keep-alive父子組件的解析

動態組件component標籤元素會在closeElement函數執行過程當中,由processComponent處理vue-router

function processComponent (el) {
    var binding;
    if ((binding = getBindingAttr(el, 'is'))) {
      el.component = binding;
    }
    if (getAndRemoveAttr(el, 'inline-template') != null) {
      el.inlineTemplate = true;
    }
}
複製代碼

生成緩存

{
  component: 'view'
}
複製代碼

即AST節點爲bash

{
  attrsList: [],
  attrsMap: {:is: "view"},
  children: [],
  component: "view",
  end: 60,
  parent: {type: 1, tag: "keep-alive", attrsList: Array(0), attrsMap: {…}, rawAttrsMap: {…}, …}
  plain: false
  rawAttrsMap: {:is: {
    end: 47,
    name: ":is",
    start: 37,
    value: "view"
  }},
  start: 26,
  tag: "component",
  type: 1
}
複製代碼

再生成代碼階段,genElement函數會調用app

var code;
if (el.component) {
  code = genComponent(el.component, el, state);
} else {
  var data;
  if (!el.plain || (el.pre && state.maybeComponent(el))) {
    data = genData$2(el, state);
  }

  var children = el.inlineTemplate ? null : genChildren(el, state, true);
  code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
}
複製代碼

genComponent具體爲dom

function genComponent (
    componentName,
    el,
    state
  ) {
    var children = el.inlineTemplate ? null : genChildren(el, state, true);
    return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")")
}
複製代碼

返回async

"_c(view,{tag:"component"})"
複製代碼

而後添加keep-alive組件生成函數

"_c('keep-alive',[_c(view,{tag:"component"})],1)"
複製代碼

最後父組件的render生成的完整形式爲

_c('div',{
    attrs:{"id":"app"}
}, [
    _c('keep-alive',
    [
        _c(view, {tag: "component"})], 1),
        _v(" "),
        _c('button',{on:{"click":changeView}},[_v("切換")
    ])
], 1)
複製代碼

其中_c表示createElem建立元素vnode, _v建立文本類型的vnode。keep-alive的子組件component變爲_c(view, {tag: "component"})], 1)而後生成vnode的過程以下:

function createElement (
    context,
    tag,
    data,
    children,
    normalizationType,
    alwaysNormalize
  ) {
    ...
    return _createElement(context, tag, data, children, normalizationType)
  }

複製代碼

data爲 {tag: "component"}, tag爲view, _createElement函數爲

...
var vnode, ns;
    if (typeof tag === 'string') {
      var Ctor;
      ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
      if (config.isReservedTag(tag)) {
        // platform built-in elements
        vnode = new VNode(
          config.parsePlatformTagName(tag), data, children,
          undefined, undefined, context
        );
      } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
        // component
        // 建立子組件的vnode
        vnode = createComponent(Ctor, data, context, children, tag);
      } else {
        // unknown or unlisted namespaced elements
        // check at runtime because it may get assigned a namespace when its
        // parent normalizes children
        vnode = new VNode(
          tag, data, children,
          undefined, undefined, context
        );
      }
    } else {
      // direct component options / constructor
      vnode = createComponent(tag, data, context, children);
    }
複製代碼

由於view1並非元素節點,故進入執行vnode = createComponent(Ctor, data, context, children, tag), 函數具體爲

function createComponent (
    Ctor,
    data,
    context,
    children,
    tag
  ) {
    ...
    data = data || {};

    // resolve constructor options in case global mixins are applied after
    // component constructor creation
    resolveConstructorOptions(Ctor);

    // extract props
    var propsData = extractPropsFromVNodeData(data, Ctor, tag);

    // functional component
    if (isTrue(Ctor.options.functional)) {
      return createFunctionalComponent(Ctor, propsData, data, context, children)
    }

    // extract listeners, since these needs to be treated as
    // child component listeners instead of DOM listeners
    var listeners = data.on;
    // replace with listeners with .native modifier
    // so it gets processed during parent component patch.
    data.on = data.nativeOn;

    if (isTrue(Ctor.options.abstract)) {
      // abstract components do not keep anything
      // other than props & listeners & slot

      // work around flow
      var slot = data.slot;
      data = {};
      if (slot) {
        data.slot = slot;
      }
    }

    // install component management hooks onto the placeholder node
    // 在data中增長insert、prepatch、init、destroy四個鉤子
    installComponentHooks(data);

    // return a placeholder vnode
    // 建立並返回vnode
    var name = Ctor.options.name || tag;
    var vnode = new VNode(
      ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
      data, undefined, undefined, undefined, context,
      { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
      asyncFactory
    );

    return vnode
  }
複製代碼

其中vnode中的componentOptions爲 {Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children} _c(view,{tag:"component"})生成vnode爲

{
  asyncFactory: undefined,
  asyncMeta: undefined,
  children: undefined,
  componentInstance: undefined,
  componentOptions: {
    Ctor: ƒ, 
    propsData: undefined, 
    listeners: undefined, 
    tag: "view1", 
    children: undefined
  },
  context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
  data: {tag: "component", on: undefined, hook: {…}},
  elm: undefined,
  fnContext: undefined,
  fnOptions: undefined,
  fnScopeId: undefined,
  isAsyncPlaceholder: false,
  isCloned: false,
  isComment: false,
  isOnce: false,
  isRootInsert: true,
  isStatic: false,
  key: undefined,
  ns: undefined,
  parent: undefined,
  raw: false,
  tag: "vue-component-1-view1",
  text: undefined,
  child: undefined
}
複製代碼

keep-alive組件

keep-alive組件是Vue內部定義的組件,它的實現也是一個對象,注意它有一個屬性 abstract 爲 true,是一個抽象組件。在初始化initLifecycle過程當中

// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
  while (parent.$options.abstract && parent.$parent) {
    parent = parent.$parent
  }
  parent.$children.push(vm)
}
vm.$parent = parent
複製代碼

組件之間創建父子關係會跳過該抽象組件,這個例子中的 keep-alive生成的VNode爲:

{
  asyncFactory: undefined,
  asyncMeta: undefined,
  children: undefined,
  componentInstance: undefined,
  componentOptions: {
    Ctor: ƒ, 
    propsData: {}, 
    listeners: undefined, 
    tag: "keep-alive", 
    children: Array(1)
  },
  context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
  data: {
    hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ}
  },
  elm: undefined,
  fnContext: undefined,
  fnOptions: undefined,
  fnScopeId: undefined,
  isAsyncPlaceholder: false,
  isCloned: false,
  isComment: false,
  isOnce: false,
  isRootInsert: true,
  isStatic: false,
  key: undefined,
  ns: undefined,
  parent: undefined,
  raw: false,
  tag: "vue-component-3-keep-alive",
  text: undefined,
  child: undefined
}
複製代碼

最後進行updateComponent操做, 在掛載階段會進行dom diff操做, 執行 patch (oldVnode, vnode, hydrating, removeOnly)

...
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
}
...

複製代碼

遞歸的建立節點而後掛載到parentElem上,對於子組件,會執行$createElement函數, 若是是普通元素節點則直接返回,過程以下:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    // i 是insert、init、prepatch、destroy的鉤子對象
    var i = vnode.data;
    if (isDef(i)) {
    var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
    if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */);
    }
    // after calling the init hook, if the vnode is a child component
    // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm.
    // in that case we can just return the element and be done.
    if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue);
        insert(parentElm, vnode.elm, refElm);
        if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
        }
        return true
    }
    }
}
複製代碼

上面i 是insert、init、prepatch、destroy的鉤子對象

// inline hooks to be invoked on component VNodes during patch
  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 {
        var child = vnode.componentInstance = createComponentInstanceForVnode(
          vnode,
          activeInstance
        );
        child.$mount(hydrating ? vnode.elm : undefined, hydrating);
      }
    },

    prepatch: function prepatch (oldVnode, vnode) {
      var options = vnode.componentOptions;
      var child = vnode.componentInstance = oldVnode.componentInstance;
      updateChildComponent(
        child,
        options.propsData, // updated props
        options.listeners, // updated listeners
        vnode, // new parent vnode
        options.children // new children
      );
    },

    insert: function insert (vnode) {
      var context = vnode.context;
      var componentInstance = vnode.componentInstance;
      if (!componentInstance._isMounted) {
        componentInstance._isMounted = true;
        callHook(componentInstance, 'mounted');
      }
      if (vnode.data.keepAlive) {
        if (context._isMounted) {
          // vue-router#1212
          // During updates, a kept-alive component's child components may // change, so directly walking the tree here may call activated hooks // on incorrect children. Instead we push them into a queue which will // be processed after the whole patch process ended. queueActivatedComponent(componentInstance); } else { activateChildComponent(componentInstance, true /* direct */); } } }, destroy: function destroy (vnode) { var componentInstance = vnode.componentInstance; if (!componentInstance._isDestroyed) { if (!vnode.data.keepAlive) { componentInstance.$destroy(); } else { deactivateChildComponent(componentInstance, true /* direct */); } } } }; 複製代碼

若是是初次建立組件,則調用init鉤子裏的

createComponentInstanceForVnode(
          vnode,
          activeInstance
        );
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
複製代碼

已存在的話更新組件調用prepatch;

建立元素實例vnode

function createComponentInstanceForVnode (
    vnode, // we know it's MountedComponentVNode but flow doesn't
    parent // activeInstance in lifecycle state
  ) {
    var options = {
      _isComponent: true,
      _parentVnode: vnode,
      parent: parent
    };
    // check inline-template render functions
    var inlineTemplate = vnode.data.inlineTemplate;
    if (isDef(inlineTemplate)) {
      options.render = inlineTemplate.render;
      options.staticRenderFns = inlineTemplate.staticRenderFns;
    }
    return new vnode.componentOptions.Ctor(options)
  }
複製代碼

_parentNode表示當前vnode, 而後就是子組件的初始化

var Sub = function VueComponent (options) {
        this._init(options);
      };
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    Sub.cid = cid++;
    Sub.options = mergeOptions(
    Super.options,
    extendOptions
    );
    Sub['super'] = Super
複製代碼

在initRender初始函數中會初始化$slots={default: [vnode]}, 由於 是在標籤內部寫 DOM,因此能夠先獲取到它的默認插槽,而後再獲取到它的第一個子節點。 只處理第一個子元素,因此通常和它搭配使用的有 component 動態組件或者是 router-view獲取須要渲染的子組件的VNode、$scopedSlots、vm.$createElement; 子組件_render執行過程會處理

vm.$scopedSlots = normalizeScopedSlots {
    ...
    return {
        default: ƒ ()
        $hasNormal: true
        $key: undefined
        $stable: false
    }
}
vnode = render.call(vm._renderProxy, vm.$createElement);
複製代碼

render函數此時指向vm._renderProxy即keep-alive內置組件, 若是當前子組件是keep-alive則執行

var KeepAlive = {
    name: 'keep-alive',
    abstract: true,
    props: {
      include: patternTypes,
      exclude: patternTypes,
      max: [String, Number]
    },
    created: function created () {
      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;

      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); });
      });
    },

    render: function render () {
      // 獲取子組件內容 [VNode]
      var slot = this.$slots.default;
      var vnode = getFirstComponentChild(slot);
      var componentOptions = vnode && vnode.componentOptions;
      if (componentOptions) {
        // check pattern
        // 得到子組件的組件名
        var name = getComponentName(componentOptions);
        var ref = this;
        var include = ref.include;
        var exclude = ref.exclude;
        // 若是知足了配置 include 且不匹配或者是配置了 exclude 且匹配,那麼就直接返回這個組件的 vnode
        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
          // 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;
          // 判讀緩存裏是否有"1::view1",存在的話直接從緩存裏獲取組件實例,更新keys中的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
          // 若是配置了 max 而且緩存的長度超過了 this.max,還要從緩存中刪除第一個
          if (this.max && keys.length > parseInt(this.max)) {
            pruneCacheEntry(cache, keys[0], keys, this._vnode);
          }
        }

        vnode.data.keepAlive = true;
      }
      return vnode || (slot && slot[0])
    }
  };
複製代碼

其中pruneCacheEntry函數爲:

function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null 
  remove(keys, key)
}
複製代碼

若是緩存的組件標籤與當前渲染組件的tag不一致時,也執行刪除緩存的組件實例的 $destroy 方法,最後設置 vnode.data.keepAlive = true。此外keep-alive還會經過watch檢測傳入的include 和 exclude 的變化,對緩存作處理即對 cache 作遍歷,發現緩存的節點名稱和新的規則沒有匹配上的時候,就把這個緩存節點從緩存中摘除。 keep-alive的子組件生成的vnode爲:

{
  asyncFactory: undefined
  asyncMeta: undefined,
  children: undefined,
  componentInstance: undefined,
  componentOptions: {
    Ctor: ƒ, 
    propsData: undefined, 
    listeners: undefined, 
    tag: "view1", 
    children: undefined
  },
  context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …},
  data: {
    tag: "component", 
    on: undefined, 
    hook: {…}, 
    keepAlive: true
  },
  elm: undefined,
  fnContext: undefined,
  fnOptions: undefined,
  fnScopeId: undefined,
  isAsyncPlaceholder: false,
  isCloned: false,
  isComment: false,
  isOnce: false,
  isRootInsert: true,
  isStatic: false,
  key: undefined,
  ns: undefined,
  parent: undefined,
  raw: false,
  tag: "vue-component-1-view1",
  text: undefined,
  child: undefined,
}
複製代碼

接着在渲染階段的createElm函數調用createComponent, 會對view1組件進行初始化並進行編譯模板生成render函數

(function anonymous(
) {
with(this){return _c('div',[_v("view component1")])}
})
複製代碼

最後是渲染過程。

首次渲染

在最後的渲染階段,createElm函數執行createComponent,會觸發componentVNodeHooks中的init鉤子,初次渲染vnode.componentInstance爲undefined,vnode.data.keepAlive設置了爲true,因此會進入else,走正常的mount流程:

// 前面設置了keep-alive屬性爲true,故vnode.data.keepAlive = true
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 {
    // 將動態組件view1掛載到父級(非keep-alive組件)
    var child = vnode.componentInstance = createComponentInstanceForVnode(
      vnode,
      activeInstance
    );
    // 子節點掛載到父組件
    child.$mount(hydrating ? vnode.elm : undefined, hydrating);
  }
}
複製代碼

逐級掛載,最後渲染到頁面。

緩存渲染過程

當一個組件切換到另外一個組件時,在patch過程當中會對比新舊vnode以及它們的子節點,而keep-alive組件的更新,首先在組件patchVnode過程當中,一個元素即將被修復時會執行prepatch鉤子函數:

if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
  i(oldVnode, vnode);
}
複製代碼

prepatch: function prepatch (oldVnode, vnode) {
      var options = vnode.componentOptions;
      var child = vnode.componentInstance = oldVnode.componentInstance;
      updateChildComponent(
        child,
        options.propsData, // updated props
        options.listeners, // updated listeners
        vnode, // new parent vnode
        options.children // new children
      );
    },
複製代碼

裏面的關鍵代碼就是執行 updateChildComponent( child, options.propsData, // updated props options.listeners, // updated listeners vnode, // new parent vnode options.children // new children );

該函數的核心代碼:
複製代碼

// renderChildren爲最新的子組件[VNode],vm.$options._renderChildren表示老的子組件[VNode]

var needsForceUpdate = !!(
      renderChildren ||               // has new static slots
      vm.$options._renderChildren ||  // has old static slots
      hasDynamicScopedSlot
    );
// resolve slots + force update if has children
if (needsForceUpdate) {
  vm.$slots = resolveSlots(renderChildren, parentVnode.context);
  vm.$forceUpdate();
}
複製代碼

resolveSlots將新的子組件的VNode賦值給vm.$slots,即

vm.$slots = {
    default: [VNode]
}
複製代碼

再進行強制更新,從新渲染

Vue.prototype.$forceUpdate = function () {
  var vm = this;
  if (vm._watcher) {
    vm._watcher.update();
  }
};
複製代碼

再次執行到createComponent函數時

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  var i = vnode.data;
  if (isDef(i)) {
    // 更新時,isReactivated爲true
    var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */);
    }
    // after calling the init hook, if the vnode is a child component
    // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm.
    // in that case we can just return the element and be done.
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue);
      insert(parentElm, vnode.elm, refElm);
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
      }
      return true
    }
  }
}
複製代碼

其中data爲:

{
    hook: {init: ƒ, prepatch: ƒ, insert: ƒ, destroy: ƒ}
    keepAlive: true,
    on: undefined,
    tag: "component",
}
複製代碼

上面的i(vnode, false /* hydrating */),執行

init: function init (vnode, hydrating) {
  if (
    vnode.componentInstance &&
    !vnode.componentInstance._isDestroyed &&
    vnode.data.keepAlive
  ) {
    // 更新過程的patch
    // kept-alive components, treat as a patch
    var mountedNode = vnode; // work around flow
    componentVNodeHooks.prepatch(mountedNode, mountedNode);
  }
}
複製代碼

在執行 init 鉤子函數的時候不會再執行組件的 mount 過程,回到createComponent函數,在 isReactivated 爲 true 的狀況下會執行 reactivateComponent 方法

function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
      var i;
      var innerNode = vnode;
      。。。
      // unlike a newly created component,
      // a reactivated keep-alive component doesn't insert itself insert(parentElm, vnode.elm, refElm); } 複製代碼

把緩存的 DOM 對象直接插入到目標元素。

相關文章
相關標籤/搜索