feb-alive

  • Making the address
  • Experience the link

Use reason to

  • Developers do not need to write data initialization logic in different hooks beforeRouteUpdate or Activated because of the difference between dynamic routing and normal routing
  • There is no need to cache page state manually, such as localStorage or sessionStorage for the current page
  • Feb-alive handles the storage and recovery of routing meta information

Why feb-Laive?

When we develop projects through Vue, do we have the following scenario requirements?

  1. /aJump to/b
  2. Back to the/aWhen you want to recover the page from the cache
  3. Jump again to/b, divided into two cases
    1. Case 1: Jump by link or push and you want to recreate/bPage instead of reading from the cache
    2. Case two: If you click the browser’s forward button, the page is still fetched from the cache.

The requirement of this scenario emphasizes the cache. The advantage of the cache is that the data and state of my last page are retained, and there is no need to pull data from the server, which greatly improves the user experience.

Try to implement page caching using keep-alive

<keep-alive>
  <router-view></router-view>
</keep-alive>
Copy the code

So easy but ideal is perfect, reality is cruel

There is a problem

– When/A jumps to/B and then jumps to/A, the data in the page is the/A page visited for the first time. Obviously, it is a link jump, but the caching effect actually appears, and what we expect is to open a new page just like the app.

  • The same is true for dynamic route redirection/page/1->/page/2Since the two pages refer to the same component, there is no change to the page when the jump occurs, since the key of the keep-alive cache is generated by the component (of course Vue provides the beforeRouteUpdate hook to refresh the data).
  • Summary: Keep-alive’s cache is component level ==, not page level ==.

Here’s an application scenario

For example, browse the articles page and visit three articles in sequence

  1. /artical/1
  2. /artical/2
  3. /artical/3

When I went back from /artical/3 to /artical/2, the page was still the content of article 3 due to component caching, so I had to re-pull the data from page 2 via beforeRouteUpdate. (Note that backstep does not trigger the component’s Activated hook because both routes render the same component, so instances are reused and reactivateComponent is not executed.)

If you want to go backwards from /artical/3 to /artical/2 and restore some of the states previously in /artical/2, then you also need to store and restore all of the state data in /artical/2 yourself.

In summary, the component-level cache implemented by Keep-Alive is not as good as what we expect, and keep-Alive does not meet our needs.

To address these issues, the Feb-alive plugin == was created

Since feb-alive is implemented based on keep-alive, let’s briefly analyze how keep-alive is implemented

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 () {// Get the default slot
    const slot = this.$slots.default
    // Get the first component, as officially stated, keep-alive requires that only one child element be rendered at the same time, if you have v-for in it it will not work.
    const vnode: VNode = getFirstComponentChild(slot)
    // Check whether there is a component option, that is, only components, for ordinary elements directly return the corresponding vNode
    constcomponentOptions: ? VNodeComponentOptions = vnode && vnode.componentOptionsif (componentOptions) {
      // Check include and exclude
      constname: ? 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
      // If a child component's key is specified, otherwise a key is generated by cid+tag
      constkey: ? string = vnode.key ==null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? ` : :${componentOptions.tag}` : ' ')
        : vnode.key
      // Determine whether there is a cache
      if (cache[key]) {
        // Reuse the component instance directly and update the key location
        vnode.componentInstance = cache[key].componentInstance
        remove(keys, key)
        keys.push(key)
      } else {
        // The vNode stored here does not yet have an instance, which will be generated later in the process by createComponent
        cache[key] = vnode
        keys.push(key)
        // When the number of caches exceeds the threshold, the earliest key is deleted
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }
      // Set the keepAlive attribute. CreateComponent determines whether a component instance has been created, and if so and keepAlive is true the Actived hook is triggered.
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])}}Copy the code

Keep-alive is an abstract component that maintains a cache in its instance, which is the following code section

created () {
  // Store component cache
  this.cache = Object.create(null)
  this.keys = []
}
Copy the code

Since route switching does not destroy keep-alive components, the cache is always present.

Continue to look at the implementation of keep-alive in cache storage and read, first use a simple demo to describe the cache of keep-alive components and the cache recovery process

let Foo = {
    template: '<div class="foo">foo component</div>'.name: 'Foo'
}
let Bar = {
    template: '<div class="bar">bar component</div>'.name: 'Bar'
}
let gvm = new Vue({
    el: '#app'.template: ` 
      
'
.components: { Foo, Bar }, data: { renderCom: 'Foo' }, methods: { change () { this.renderCom = this.renderCom === 'Foo' ? 'Bar': 'Foo'}}})Copy the code

In the above example, the template of the root instance is compiled into the following render function

function anonymous(
) {
  with(this){return _c('div', {attrs: {"id":"#app"}},[_c('keep-alive',[_c(renderCom,{tag:"component"})].1),_c('button', {on: {"click":change}})],1)}}Copy the code

Online compilation is available:Cn.vuejs.org/v2/guide/re…

As you can see from the render function above, the process of vNode generation is deeply recursive, with the child vNode created first and then the parent vNode created. So when the keep-alive vNode is generated for the first rendering, Foo’s vNode is already generated and passed in as an argument to the Keep-alive vnode constructor (_c).

_c('keep-alive',[_c(renderCom,{tag:"component"})
Copy the code

The vNodes of the keep-alive component are as follows

{
    tag: 'vue-component-2-keep-alive'.children: undefined.componentInstance: undefined.componentOptions: {
        Ctor: f VueComponent(options),
        children: [Vnode],
        listeners: undefined.propsData: {},
        tag: 'keep-alive'
    },
    context: Vue {... },// Call the component instance of $createElement/_c, which is the root component instance object
    data: {
       hook: {
           init: f,
           prepatch: f,
           insert: f,
           destroy: f
       } 
    }
}

Copy the code

Note that the vNode of the component does not have children. Instead, the original Children is used as the children attribute of vNode’s componentOptions. ComponentOptions will be used when the component is instantiated. At the same time at the time of initialization componentOptions. Children will eventually be assigned to the vm. $slots, source code section below

/ / the createComponent function
function createComponent (Ctor, data, context, children, tag) {
    // Omit some code here.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
}
Copy the code

The Vue is eventually rendered using the Patch function, which converts the VNode into a real DOM, and the component is rendered using createComponent

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    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}}}Copy the code

The following is a two-step introduction

  1. The rendering of the Keep-alive component itself
  2. Keep-alive wraps the rendering of the components, Foo and Bar in this case

Let’s start with the rendering of the Keep-Alive component itself in this example

  1. Root component instantiation
  2. The root component $mount
  3. The root component calls mountComponent
  4. The root component generates renderWatcher
  5. The root component calls updateComponent
  6. The root component calls vm.render() to generate the root component vNode
  7. The root component calls vm.update(vnode)
  8. The root component calls vm.patch(oldVnode, vnode)
  9. The root component calls createElm(vnode)
  10. CreateComponent (vNode) is called when a vNode of the component type is encountered during children rendering, and it is during this process that the child component is instantiated and mounted ($mount).

So when createElm(keepAliveVnode) is executed, the keep-alive component is instantiated and mounted. During the instantiation, the vNode of the keep-alive wrapped child is assigned the $slot property of the keep-alive component instance. When the keep-alive instance calls the render function, we can call this.$slot to get the vNode of the wrapped component, which in demo is the vNode of the Foo component

render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    constcomponentOptions: ? VNodeComponentOptions = vnode && vnode.componentOptionsif (componentOptions) {
      constname: ? 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
      constkey: ? 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])}Copy the code

CreateElm (keepAliveVnode) instantiates keep-alive and mounts the keep-alive component ($mount). This.$slot can be used to retrieve a vNode for a child of a keep-alive component. This.$slot can be used to retrieve a vNode for a child of a keep-alive component.

<keep-alive> <Foo /> <Bar /> </keep-alive> // only Foo components are renderedCopy the code

ComponentOptions (Foo, vNode, vnode, vnode, vnode, vnode, vnode, vnode, vnode, vnode, vnode, vnode, vnode, vnode) Exclude indicates that any matching components will not be cached. Relevant rules are not set in demo. Therefore, ignore them here.

const { cache, keys } = this
Copy the code

Cache and keys are generated in the CREATE hook of the Keep-alive component to store instances of components cached by the Keep-alive component and corresponding VNode keys

created () {
    this.cache = Object.create(null)
    this.keys = []
}
Copy the code

Continue to the following

constkey: ? 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)
    }
}
Copy the code

First, remove the vnode key, if the vnode. Use the vnode. The key is the key, there is no use componentOptions. Ctor. Cid + (componentOptions. Tag? ::${componentOptions.tag} : If we do not specify the key of the component, the same cache will be matched for the same component. This is why we initially described Keep-Alive as a component-level cache solution.

If the cache and keys are empty for the first rendering, else logic will be applied

cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
  pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
Copy the code

Foo vNode (note that there is no componentInstance on vNode at this time) is stored with the key as the cache key. This uses the principle of object storage. Vnode.componentinstance will be assigned to the Foo instance when it is instantiated, so that the vnode.componentinstance will be available when the keep-alive component render next time.

So the first rendering is just a Vnode that stores the wrapped component Foo in the keep-alive cache.

Render for wrapped components

The render function (keep-alive) returns the vnode of the Foo component, so when the keep-alive patch is executed, an instance of Foo will be created, and then the Foo component will be mounted. This process is no different from normal components and will not be covered here.

When a component switches from Foo to Bar

In this example, renderCom attribute changes will trigger the renderWatcher of the root component, and then patch(oldVnode, vnode) will be executed. When comparing child vnode, The old and new VNodes of keep-alive will be judged as samevnodes, and then the logic of patchVnode will be entered

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    if (oldVnode === vnode) {
      return
    }
    // Omit code here.var i;
    var data = vnode.data;
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode);
    }
    // Omit code here. }Copy the code

Since our keep-alive is a component, some lifecycle hooks are injected at vNode creation, including prepatch hooks, which are coded as follows

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
    );
}
Copy the code

Thus, the keep-alive instance will be reused during the re-rendering of the root component, which ensures that the previous cache on the keep-alive instance will still exist

var child = vnode.componentInstance = oldVnode.componentInstance;
Copy the code

The following updateChildComponent function is the key to switching from Foo to Bar. We know that since the keep-alive component is reused here, initRender is no longer triggered, so vm.$slot is not updated again. So the slot update is done in the updateChildComponent function

function updateChildComponent (vm, propsData, listeners, parentVnode, renderChildren) {
  if(process.env.NODE_ENV ! = ='production') {
    isUpdatingChildComponent = true;
  }

  // determine whether component has slot children
  // we need to do this before overwriting $options._renderChildren
  varhasChildren = !! ( renderChildren ||// has new static slots
    vm.$options._renderChildren ||  // has old static slots
    parentVnode.data.scopedSlots || // has new scoped slotsvm.$scopedSlots ! == emptyObject// has old scoped slots
  );

  // ...

  // resolve slots + force update if has children
  if (hasChildren) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context);
    vm.$forceUpdate();
  }

  if(process.env.NODE_ENV ! = ='production') {
    isUpdatingChildComponent = false; }}Copy the code

The updateChildComponent function updates the functions, Listeners, and slots of the current component instance. We’ll focus on the Slots update, where resolveSlots is used to fetch the vNode of the latest wrapped component, the Bar component in the demo, and then vm.$forceUpdate is used to force the keep-alive component to re-render. (Hint: When our component has slots, the component instance $fourceUpdate is triggered by the component’s parent re-render. There is a performance penalty because the mandatory update is triggered regardless of whether the slot is affected by data changes, which will be optimized in 3.0, according to vueConf.), for example

// Home.vue
<template>
    <Artical>
        <Foo />
    </Artical>
</tempalte>
Copy the code

In this example, when the Home component is updated, a forced refresh of the Artical component is triggered, which is unnecessary.

After updating the slots of the Keep-alive instance, the forceUpdate of the Keep − Alive instance is triggered directly from the slots of the Keep-alive instance. Then enter the render function of keep-alive again

render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    // ...
}
Copy the code

The render function retrieves the vNode as the vnode of the Bar component, and the following process is the same as Foo rendering, except that the vnode of the Bar component is cached in the keep-Alive instance’s cache object.

When the component switches from Bar to Foo again

The logic for the keep-alive component is the same as described above

  1. Perform prepatch
  2. Reuse keep-alive component instances
  3. Execute updateChildComponent to update $slots
  4. Trigger vm. $forceUpdate
  5. Trigger the keep-alive component render function

Enter the render function again, and the cache[key] will match the vNode cached when Foo was first rendered

constkey: ? 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)
    }
}
Copy the code

Because the keep-alive component is Foo, the key generated is the same as the key generated when Foo was first rendered, so the keep-alive function enters the first if branch, which matches the cache[key]. Assign cached componentInstance to the current VNode, and update keys(when Max is present, it’s guaranteed to remove older caches).

Now, a lot of you might ask, what’s the point of setting vnode.componentInstance? This involves the source code section of Vue.

Since Foo is a Vue component, it will enter createComponent, so it will enter the following function fragment

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    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}}}Copy the code

According to the keep-alive source code above, isReactivated is true, and then the lifecycle init function hangs when the VNode is generated

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); },... }Copy the code

Since the instance already exists and keepAlive is true, the first if logic is executed, prepatch is executed, the component properties are updated, some listeners are updated, and slots are updated if there are slots, and $forceUpdate is executed.

Continuing createComponent, initComponent and insert are executed inside the function

if (isDef(vnode.componentInstance)) {
    // Assign the DOM on the instance to vNode
    initComponent(vnode, insertedVnodeQueue);
    / / insert the dom
    insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
    reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
Copy the code

At this point, when the component switches from Bar to Foo again, both the instance and DOM are reused for a great experience! The feb-alive implementation we will implement later is based on keep-alive.

Vue page-level Cache Solution Feb-Alive (part 2)

Reference documentation

  • vue-navigation
  • Vue. Js technology revealed