preface
As we develop projects, we encounter the need for component instances of those tags to be cached when they are first created. To solve this problem, we often wrap it with a <keep-alive> element. This improves the user experience by enabling caching for subsequent access to the component. <keep-alive> <router-view> <router-view> <keep-alive> <router-view> <router-view> <keep-alive> <router-view>
keep-alive
is a built-in component of Vue, which is defined in SRC /core/components/keep-alive.js:
// src/core/components/keep-alive.js
export default {
name: 'keep-alive'.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[keyToCache] = {
name: getComponentName(componentOptions),
tag,
componentInstance,
}
keys.push(keyToCache)
// 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()
this.$watch('include'.val= > {
pruneCache(this.name= > matches(val, name))
})
this.$watch('exclude'.val= > {
pruneCache(this.name= >! matches(val, name)) }) }, updated () {this.cacheVNode()
},
render () {
// Used to access content distributed by the slot.
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
constcomponentOptions: ? VNodeComponentOptions = vnode && vnode.componentOptionsif (componentOptions) {
// check pattern
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
// 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
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
// delay setting the cache until update
this.vnodeToCache = vnode
this.keyToCache = key
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])}}Copy the code
The <keep-alive> component is an object. The main function of the <keep-alive> component is to cache vNodes that have been created. In addition, it implements some Props:
- Include – a string or regular expression. Only components with matching names are cached.
- Exclude – a string or regular expression. Any component with a matching name will not be cached.
- Max – Numbers. Maximum number of component instances can be cached.
We’ll see that it also implements the render method itself, which executes the render function when executing the <keep-alive> component:
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
constcomponentOptions: ? VNodeComponentOptions = vnode && vnode.componentOptionsCopy the code
The render function logic gets its default slot first, then gets its first child node and performs some processing logic. It is usually paired with component dynamic components or router-views.
We notice that it has an abstract: true attribute, indicating that this is an abstract component. It is not mentioned in the official documentation, but it is important to know that <keep-alive> caches inactive component instances when wrapping dynamic components, rather than destroying them. Like <transition>, <keep-alive> is an abstract component: it does not render a DOM element on its own, nor does it appear in the component’s parent chain. This can be verified in the Vue source code by the initLifecycle method in lifecycle. Js:
// src/core/instance/lifecycle.js
// Code snippet
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if(parent && ! options.abstract) {// Abstract components are ignored when the component instance establishes parent-child relationships
while(parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) } vm.$parent = parent vm.$root = parent ? parent.$root : vm vm.$children = [] vm.$refs = {} vm._watcher =null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
Copy the code
The component rendering
To demonstrate, I created a project called Keep-Alive to illustrate. The following is the general directory structure of my project:
Heavy Metal Flag School - - - - - - - - - - - - - - - - - - - - - - - - │ └ ─ index. The HTML └ ─ SRC ├ ─ App. Vue ├ ─ assets │ └ ─ logo. The PNG ├ ─ components │ ├ ─ TAB - archive. Vue │ └ ─ TAB - posts. Vue ├ ─ Lifecycle. Js ├ ─ main. Js ├ ─ the router │ └ ─ index. The js ├ ─ styles │ └ ─ index. The CSS └ ─ views ├ ─ PageHome. Vue └ ─ PageIndex. VueCopy the code
My project mainly consists of two level 1 pages index and home. The Index page is a content generated by a dynamic component, and the Home Page is a content presentation containing two child pages.
index page
home page
Take a look at my router.js
// src/router/index.js
import Vue from "vue";
import VueRouter from "vue-router";
import PageIndex from "@/views/PageIndex";
import PageHome from "@/views/PageHome";
Vue.use(VueRouter);
const routes = [
{
path: "/".component: PageIndex,
},
{
path: "/home".component: PageHome,
}
]
export default new VueRouter({
routes,
})
Copy the code
There is also a lifecycle. Js that outputs created, Activated, deactivated, Destroyed, and other related lifecycle hooks for each Vue component instance in the console.
// src/lifecycle.js
export default {
created(){
console.log(`The ${this.$options.name} created`);
},
mounted() {
console.log(`The ${this.$options.name} mounted`);
},
activated() {
console.log(`The ${this.$options.name} activated`);
},
deactivated() {
console.log(`The ${this.$options.name} deactivated`);
},
destroyed() {
console.log(`The ${this.$options.name} destroyed`); }}Copy the code
Do not use the keep alive
Let’s start with the app.vue file
// src/App.vue
<template>
<div id="app">
<div class="navigation">
<router-link to="/">to index</router-link>
<router-link to="/home">to home</router-link>
</div>
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
</template>
<script>
export default {
name: 'App',}</script>
<style>
</style>
Copy the code
Open our page after NPM run serve. Let’s start by clicking on Hipster Ipsum in the index TAB:
Switch from index to the home page to see the console output:
index created
index mounted
home created
index destroyed
home mounted
Copy the code
Note that the Index component implements the Destroyed hook function.
Now let’s see what output comes from cutting back from the Home Page to the Index Page console:
index created
home destroyed
index mounted
Copy the code
The Home component executes the Destroyed hook function and the Index component executes the Created and Mounted function.
This would explain why the contents of Hipster Ipsum we looked at earlier in the Index page were rerendered.
Ok, now let’s add a new requirement: the page “remembers” the last time we viewed it as we toggle back and forth between the Index page and the home page.
Well, it’s not too easy! Go straight to keep-alive!
Use keep – the alive
We use keep-alive to cache our components, so we can modify the app. vue file:
// src/App.vue <template> <div id="app"> <div class="navigation"> <router-link to="/">to index</router-link> <router-link to="/home">to home</router-link> </div> <! -- Use the keep-alive cache component --> <keep-alive> <router-view></router-view> </keep-alive> </div> </template> <script> export default { name: 'App', } </script> <style> </style>Copy the code
Click on the Hipster Ipsum TAB of the Index page, then click on the Home page, and finally return to the Index page.
We can see that the component has been cached, and the easiest way to imagine this is that the TAB contents we previously viewed in the Index component have not been reset. And that’s what we need!
We can analyze it in two cases:
- The first case is to transition from the Index component to the home component
- Created, Mounted, and activated are the lifecycle hook functions implemented by index
- The home component then executes the lifecycle hook Created
- The index group price then executes the lifecycle hook deactivated
- Mounted and Activated of the home component
index created
index mounted
index activated
home created
index deactivated
home mounted
home activated
Copy the code
- The second case is to cut from the home component back to the index component
- Start by executing the deactivated lifecycle hook of the home component
- Then execute the index group value of the lifecycle hook Activated
home deactivated
index activated
Copy the code
To summarize what happened in the demo: Created, Mounted, and activated hooks are created, mounted, and activated hooks are executed when the keep-alive component is first rendered. The difference between keep-alive rendering and normal rendering is that keep-alive caches the component’s VNode. Finally, the index component is not destroyed when it is inactivated but cached by the Keep-alive component when it executes the Deactivated hook. The Activated life cycle hook function is called when a child component is reused.
Breakpoint debugging keep-alive
Let’s take a look at this through breakpoint debugging.
First found in our project node_modules vue/dist/vue. Runtime. Esm. Js, open the file and search for “KeepAlive” components. We in the render method debugger (probably in vue. Runtime. Esm. Line 5380 of js file location), and componentVNodeHooks. Insert and componentVNodeHooks. Destroy Debugger (line 3161 and 3176 respectively)
// node_modules/vue/dist/vue.runtime.esm.js
render: function render () {
debugger
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;
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;
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
remove(keys, key);
keys.push(key);
} else {
// delay setting the cache until update
this.vnodeToCache = vnode;
this.keyToCache = key;
}
vnode.data.keepAlive = true;
}
return vnode || (slot && slot[0])}Copy the code
// node_modules/vue/dist/vue.runtime.esm.js
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) {
debugger
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) {
debugger
var componentInstance = vnode.componentInstance;
if(! componentInstance._isDestroyed) {if(! vnode.data.keepAlive) { componentInstance.$destroy(); }else {
deactivateChildComponent(componentInstance, true /* direct */); }}}};Copy the code
Keep-alive first render
Rerun the project and open the Index component:
When render is performed on the keep-alive component, it gets the FirstComponentChild (our Index component) to execute
this.vnodeToCache = vnode;
this.keyToCache = key;
vnode.data.keepAlive = true;
Copy the code
KeepAlive = true; vnode.data.keepAlive = true;
Let’s move on to the activateChildComponent(componentInstance, true /* direct */); The Activated lifecycle hook of the Index component is called.
We see the activateChildComponent definition in the source code:
// src/core/instance/lifecycle.js
export function activateChildComponent (vm: Component, direct? : boolean) {
if (direct) {
vm._directInactive = false
if (isInInactiveTree(vm)) {
return}}else if (vm._directInactive) {
return
}
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
callHook(vm, 'activated')}}Copy the code
Since the activateChildComponent(componentInstance, true /* direct */) is passed with the second argument true, Therefore, a value of _directInactive = false is set on the instance of the Index component. And since the initial value of Vm. _inactive is null when initLifecycle (VM) is executed, callHook(vm, ‘activated’) is also executed. This is why the Index component executes the Activated lifecycle hook.
After the above execution, we see the console output:
index created
index mounted
index activated
Copy the code
So for the first rendering of a keep-alive wrapped component, in addition to creating a cache in keep-alive, the component implements both created and Activated lifecycle hooks.
Keep-alive cache rendering
Finally, click to Index to return from home Page to Index Page. The home component is deactivated and the Index component is activated.
We continue until the destroy hook of the home component is executed. Note that vnode.data.keepAlive is true at this time. So instead of calling the component’s $destroy() method, the destroy logic goes to deactivateChildComponent(componentInstance, true /* direct */); Logic.
The source for the deactivateChildComponent method is also defined in the lifecycle. Js file:
// src/core/instance/lifecycle.js
export function deactivateChildComponent (vm: Component, direct? : boolean) {
if (direct) {
vm._directInactive = true
if (isInInactiveTree(vm)) {
return}}if(! vm._inactive) { vm._inactive =true
for (let i = 0; i < vm.$children.length; i++) {
deactivateChildComponent(vm.$children[i])
}
callHook(vm, 'deactivated')}}Copy the code
So executing deactivateChildComponent(componentInstance, true /* direct */) sets vm._directInactive to true. The _inactive value of the home component is set to true. Finally, callHook(vm, ‘deactivated’) is executed. So the console will print home deactivated.
Finally, we see that the index component is actually a diff between the old and new VNodes. The diff process is to execute patchVnode, which first executes the hook function of Prepatch, which executes the updateChildComponent method. The updateChildComponent method triggers the keep-alive component to execute the $forceUpdate logic, which re-executes the Keep-alive render method. When the keep-alive component executes the render method, if the first component vnode it wraps hits the cache, it returns the vnode.componentInstance in the cache. In our case, the cached Index component is followed by the patch process, which executes again to the createComponent method. When createComponent is executed again, vnode.componentInstance and isReactivated of the component instance are both true. Therefore, the component mount process is no longer performed when the init hook function is executed.
So we see that the console just prints:
home deactivated
index activated
Copy the code
- UpdateChildComponent triggers the keep-alive component to execute the $forceUpdate logic:
- The keep-alive component reexecutes the render method of keep-alive by executing $forceUpdate:
- Execute render and index hits the cache:
- When createComponent is executed again, vnode.componentInstance and isReactivated of the component instance are both true. Therefore, the component mount process is no longer performed when the init hook function is executed. This explains why index created and Index Mounted are not executed when returning from the home component to the Index component.
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
// 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); }},// ...
};
Copy the code
Prepatch:
// src/core/vdom/create-component.js
//inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
/ /...
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const 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
ReactivateComponent will eventually execute insert(parentElm, vNode.elm, refElm);
Insert (parentElm, vNode.elm, refElm) inserts the cached index component directly into the target element, thus completing the index component rendering process.
function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i;
// hack for #4339: a reactivated component with inner transition
// does not trigger because the inner node's created hooks are not called
// again. It's not ideal to involve module-specific logic in here but
// there doesn't seem to be a better way to do it.
var innerNode = vnode;
while (innerNode.componentInstance) {
innerNode = innerNode.componentInstance._vnode;
if (isDef(i = innerNode.data) && isDef(i = i.transition)) {
for (i = 0; i < cbs.activate.length; ++i) {
cbs.activate[i](emptyNode, innerNode);
}
insertedVnodeQueue.push(innerNode);
break}}// unlike a newly created component,
// a reactivated keep-alive component doesn't insert itself
insert(parentElm, vnode.elm, refElm);
}
Copy the code
Router-view processing logic for cache component rendering
Then we go to vue-router/dist/vue-router.esm.js in node_modules, open the file and search for the “RouterView” component. We found the while statement in the render method to debugger the keep-alive processing logic. (Approximately 331 and 343)
// vue-router/src/components/view.js
render (_, { props, children, parent, data }) {
// used by devtools to display a router-view badge
data.routerView = true
// directly use parent context's createElement() function
// so that components rendered by router-view can resolve named slots
const h = parent.$createElement
const name = props.name
const route = parent.$route
const cache = parent._routerViewCache || (parent._routerViewCache = {})
// Determine the current view depth and check whether the tree has been switched to inactive but remains active.
let depth = 0
let inactive = false
// Find the parent component until it is a Vue instance. The main logic here is to determine the nesting hierarchy of the routerView and whether there are cache components
while(parent && parent._routerRoot ! == parent) {const vnodeData = parent.$vnode ? parent.$vnode.data : {}
if (vnodeData.routerView) {
depth++
}
Cache rendering is used when keep-alive is used and both _directInactive and _inactive of the parent component are true
if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
debugger
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
// Render the previous view if the tree is inactivated and remains active
if (inactive) {
const cachedData = cache[name]
const cachedComponent = cachedData && cachedData.component
if (cachedComponent) {
debugger
/ / # 2301
// pass props
if (cachedData.configProps) {
fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
}
return h(cachedComponent, data, children)
} else {
// render previous empty view
return h()
}
}
// ...
}
Copy the code
We then add a child to the home component, home-Child.
<template>
<div>home child: {{$route.params.id}}</div>
</template>
<script>
import lifecycle from '@/lifecycle'
export default {
mixins: [lifecycle],
name:"homeChild",
beforeRouteUpdate (to, from, next) {
console.log('to.params.id: ', to.params.id);
next()
}
}
</script>
<style>
</style>
Copy the code
And add a child route to the home component. Router /index.js after modification:
import Vue from "vue";
import VueRouter from "vue-router";
import PageIndex from "@/views/PageIndex";
import PageHome from "@/views/PageHome";
import homeChild from "@/components/home-child";
Vue.use(VueRouter);
const routes = [
{
path: "/".name: "index".component: PageIndex,
},
{
path: "/home".name: "home".component: PageHome,
children: [{path: "homeChild/:id".name: "homeChild".component: homeChild,
},
],
},
];
export default new VueRouter({
routes,
});
Copy the code
Add a router-view to the home component:
// src/views/PageHome.vue
<template>
<div>
<h1>HOME PAGE</h1>
<router-view></router-view>
</div>
</template>
<script>
import lifecycle from '@/lifecycle';
export default {
name: "home".mixins: [lifecycle]
}
</script>
<style>
</style>
Copy the code
App.vue:
<template> <div id="app"> <div class="navigation"> <router-link to="/">to index</router-link> <router-link to="/home">to home</router-link> <router-link to="/home/homeChild/foo">/home/homeChild/foo</router-link> </div> <keep-alive> <router-view></router-view> </keep-alive> </div> </template> <script> export default { name: 'App', } </script> <style> </style>Copy the code
To re-run the project, let’s first switch from the “/ “path to the” /home/homechild /foo” path. Then open the console and finally click “to Index” to return to the Index component. The breakpoint stops at vnodeData.keepAlive && Parent._directInactive && Parent._inactive.
Inactive is set to true. And the code executes until inactive = true. We see that the Router-view component of homeChild uses cache rendering.
Source link
keep-alive-demo