preface
The most common approach to performance optimization is caching. Caching frequently accessed resources reduces the time or memory consumption by reducing requests or initialization. Vue provides us with a cache component, keep-Alive, which can be used for routing level or component-level caching.
But do you know how caching works and how component cache rendering works? So this article will analyze the principle of keep-alive.
LRU strategy
When using keep-alive, you can add prop attributes include, exclude, and Max to allow conditional caching of components. Since there are constraints, old components need to be removed from the cache, and new components need to be added to the latest cache, how to formulate the corresponding policy?
The Least recently used LRU policy filters out data based on historical data access records. The DESIGN principle of the LRU policy is that if a piece of data has not been accessed in the recent past, it is highly unlikely that it will be accessed in the future. That is, when the limited space is full of data, the data that has not been accessed for the longest should be eliminated.
- Now the cache allows only 3 components at most, and ABC components enter the cache one by one without any problems
- When component D is accessed, the memory space is insufficient. A is the earliest and oldest component to enter, so component A is removed from the cache and component D is added to the latest location
- When component B is accessed again, since B is still in the cache, B moves to the latest location, with the other components one bit later
- When E component is accessed, the memory space is insufficient, C becomes the oldest unused component, C component is removed from the cache, and E component is added to the latest location
The keep-alive cache mechanism sets the freshness of cached components according to the LRU policy, and removes components from the cache that have not been accessed for a long time. Now that you understand the caching mechanism, go into the source code to see how the Keep-alive component is implemented.
Component Implementation Principle
SRC /core/components/keep-alive.js
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 () {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 {
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
}
return vnode || (slot && slot[0])}}Copy the code
Kepp-alive is actually an abstract component that only deals with wrapped child components. It does not have a parent-child relationship with the child components and does not render them as nodes on the page. Setting abstract to true at the beginning of the component indicates that the component is abstract.
/ / source location: SRC/core/instance/lifecycle. Js
export function initLifecycle (vm: Component) {
const options = vm.$options
// 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
// ...
}
Copy the code
So how does an abstract component ignore this relationship? During initialization, initLifecycle is called to determine if the parent is an abstract component, and if it is, the parent level above the abstract component is chosen, ignoring the hierarchical relationship between the abstract component and its children.
Back to the keep-alive component, instead of writing a template, the render function determines the render result.
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
Copy the code
If keep-alive has multiple child elements, keep-alive requires that only one child element be rendered at the same time. So at the beginning we get the child element in the slot, and we call getFirstComponentChild to get the VNode of the first child element.
// 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
}
function matches (pattern: string | RegExp | Array<string>, name: string) :boolean {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1
} else if (typeof pattern === 'string') {
return pattern.split(', ').indexOf(name) > -1
} else if (isRegExp(pattern)) {
return pattern.test(name)
}
return false
}
Copy the code
If the component name does not match include or exclude, it will exit directly and return to VNode without using the caching mechanism.
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 {
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
Copy the code
The matching condition goes into the cache mechanism’s logic. If the cache is hit, the cache instance is fetched from the cache and set to the current component, and the key position is adjusted to put it last. If not, cache the current VNode and add the key to the current component. If the number of cached components exceeds the value of Max, that is, the cache space is insufficient, then pruneCacheEntry is called to remove the oldest component from the cache, that is, the component of keys[0]. The component’s keepAlive flag is then set to true, indicating that it is cached.
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)
}
Copy the code
PruneCacheEntry removes the component from the cache by calling the component $destroy method, emptying the cache component, and removing the corresponding key.
mounted () {
this.$watch('include'.val= > {
pruneCache(this.name= > matches(val, name))
})
this.$watch('exclude'.val= > {
pruneCache(this.name= >! matches(val, name)) }) }function pruneCache (keepAliveInstance: any, filter: Function) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
constcachedNode: ? VNode = cache[key]if (cachedNode) {
constname: ? string = getComponentName(cachedNode.componentOptions)if(name && ! filter(name)) { pruneCacheEntry(cache, key, keys, _vnode) } } } }Copy the code
When keep-alive is mounted, it listens for include and exclude changes, adjusts the order of cache and keys when attributes change, and ultimately calls pruneCacheEntry.
Summary: Cache is used to cache components, keys store components’ keys, and cache components are adjusted according to THE LRU policy. Keep-alive’s render returns the VNode of the component at the end, so we can also conclude that keep-alive does not actually render, but that the rendered object is a child of the wrapped component.
Component rendering process
Warm tips: This part needs to have an understanding of the render and patch process
The main two processes in the rendering process are render and patch. Before render, there will be template compilation. The render function is the product of template compilation, which is responsible for building the VNode tree, and the constructed VNode will be passed to patch. Patch generates a real DOM node tree according to the relationship between VNodes.
This diagram describes the flow of Vue view rendering:
After the completion of VNode construction, it will eventually be converted into the real DOM, and patch is a necessary process. To better understand the process of component rendering, it is assumed that keep-Alive includes two components, A and B, and A is displayed by default.
Initialize render
CreateComponent is executed to mount the component during the patch process, and component A is no exception.
// source: SRC /core/vdom/patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const 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
IsReactivated indicates whether a component isReactivated. At the time of initial rendering, A component has not been initialized, so componentInstance is undefined. The keepAlive of A component is true, because keep-alive, as the parent wrapped component, will be mounted before A component, that is, kepp-alive will perform the render process first, and A component is cached. After that, the keepAlive value of the first component in the slot (A component) is set to true. Therefore, isReactivated is false.
The component is then initialized by calling init, which is a hook function of the component:
SRC /core/vdom/create-component.js
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ? boolean {if( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) {// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
// ...
}
Copy the code
CreateComponentInstanceForVnode will new component instance and assign values to the componentInstance Vue construction, then call $mount mount components.
Back to createComponent, continue with the following logic:
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
Call initComponent to assign vNode. elm to the real DOM, and then call INSERT to insert the component’s real DOM into the parent element.
So in the initial rendering, keep-alive caches the A component and renders the A component normally.
Cache rendering
When you switch to component B and then switch back to component A, component A’s hit cache is reactivated.
After going through the patch process again, keep-alive gets the current component according to the slot, so how does the content of the slot update the cache?
const isRealElement = isDef(oldVnode.nodeType)
if(! isRealElement && sameVnode(oldVnode, vnode)) {// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null.null, removeOnly)
}
Copy the code
During uninitialized rendering, Patch calls patchVnode to compare the old and new nodes.
// source: SRC /core/vdom/patch.js
function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
// ...
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// ...
}
Copy the code
The hook function prepatch is called in patchVnode.
SRC /core/vdom/create-component.js
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
The key update method is updateChildComponent, which updates the instance properties:
/ / source location: SRC/core/instance/lifecycle. Js
export function updateChildComponent (
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
// ...
// Any static slot children from the parent may have changed during parent's
// update. Dynamic scoped slots may also have changed. In such cases, a forced
// update is necessary to ensure correctness.
constneedsForceUpdate = !! ( 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()
}
}
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
Vm._update (vm._render)
vm._watcher.update()
}
}
Copy the code
NeedsForceUpdate is true only if there is a slot, and keep-alive is the condition. First call resolveSlots to update the slots for keep-alive, then call $forceUpdate to rerender keep-alive and render again. Since the A component is cached during initialization, keep-alive directly returns the cached A component VNode. After the VNode is ready, it comes to the patch phase.
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const 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 A component goes through createComponent again, calling init.
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ? boolean {if( vnode.componentInstance && ! vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) {// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
}
Copy the code
Instead of following the logic of $mount, prepatch is called to update the instance properties. Created and Mounted lifecycle functions are not executed when the cache component is activated.
Return to createComponent where isReactivated is true and call reactivateComponent:
function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let 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.
let 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
Finally, insert is called to insert the COMPONENT’s DOM node, and the cache rendering process is complete.
Summary: Keep-alive caches components when they are first rendered. When the cache renders, keep-Alive updates the slot contents, after which $forceUpdate re-renders. This gets the latest component at render and returns the VNode from the cache if hit.
conclusion
The keep-alive component is an abstract component, which will skip the abstract component when corresponding to the parent-child relationship. It only deals with the wrapped child component, mainly caching the component VNode according to the LRU policy, and finally returning the VNode of the child component in render. The cache rendering process updates the Keep-alive slot, rerenders it, and reads the previous component VNode from the cache to implement the state cache.
Previous related articles:
Touch your hand to understand the Vue responsive principle
Hand touching takes you to understand the Computed principles of Vue
Touch your hand to understand the principle of Vue Watch
Vue you have to know about the asynchronous update mechanism and the nextTick principle
Vue view rendering principle analysis, from building vNodes to generating a real node tree