1, Foreword
The contents of this paper include:
- Keep alive usage: dynamic component & Vue router
- Keep alive source code analysis
- Hook of keep alive component and its package component
- Rendering of keep alive component and its package component
2, Introduction and application of keep alive
2.1 what is keep alive
Keep alive is an abstract component: it does not render a DOM element itself, nor does it appear in the parent component chain; When you wrap dynamic components with keep alive, inactive component instances are cached rather than destroyed.
A scene
The user selects the filter criteria on a list page to filter out a data list, and then enters the data details page from the list page, and then returns to the list page. We hope that the list page can retain the user's filter (or selected) status.
Keep alive is used to solve this scenario. Of course, keep alive is not only able to save the state of pages / components, but also can avoid repeated creation and rendering of components and effectively improve the system performance. In general, keep alive is used to save the rendering state of components.
Keep alive usage
- Application in dynamic components
<keep-alive :include="whiteList" :exclude="blackList" :max="amount"> <component :is="currentComponent"></component> </keep-alive>
- Application in Vue router
<keep-alive :include="whiteList" :exclude="blackList" :max="amount"> <router-view></router-view> </keep-alive>
include defines the cache whitelist, and keep alive caches the hit components; exclude defines the cache blacklist, and the hit components will not be cached; max defines the upper limit of the cache component. If the upper limit is exceeded, replace the cache data with the policy of LRU.
memory management A page replacement algorithm for those in memory but not used data block (internal memory block) is called LRU. The operating system will move out of memory according to which data belongs to LRU to make room to load other data.
3, Source code analysis
keep-alive.js also defines some tool functions. We can hold it down and look at the exposed objects first
// src/core/components/keep-alive.js export default { name: 'keep-alive', abstract: true, // The key to judge whether the virtual dom of the current component is rendered as a real dom props: { include: patternTypes, // Cache whitelist exclude: patternTypes, // Cache blacklist max: [String, Number] // Cached components }, created() { this.cache = Object.create(null) // Cache virtual dom this.keys = [] // The key collection of the cached virtual dom }, destroyed() { for (const key in this.cache) { // Delete all caches pruneCacheEntry(this.cache, key, this.keys) } }, mounted() { // Monitor the changes of black-and-white list in real time this.$watch('include', val => { pruneCache(this, name => matched(val, name)) }) this.$watch('exclude', val => { pruneCache(this, name => !matches(val, name)) }) }, render() { // Omit } }
It can be seen that, like the process of defining components, we first set the component name to keep alive, and then define an abstract attribute with the value of true. This attribute is not mentioned in vue's official tutorial, but it is very important. It will be used in the later rendering process. The props attribute defines all the parameters supported by the keep alive component.
Keep alive defines three hook functions in its life cycle:
- created
Initialize two objects to cache the key set corresponding to VNode (virtual DOM) and VNode respectively - destroyed
Delete the VNode instance cached in this.cache. We notice that this is not simply to set this.cache to null, but to traverse and call the pruneCacheEntry function to delete it.
// src/core/components/keep-alive.js function pruneCacheEntry ( cache: VNodeCache, key: string, keys: Array<string>, current?: VNode ) { const cached = cache[key] if (cached && (!current || cached.tag !== current.tag)) { cached.componentInstance.$destroyed() // Execute the destroy hook function of the component } cache[key] = null remove(keys, key) }
Deleting the cached VNode also requires the destroy hook function of the corresponding component instance
- mounted
Listen for the include and exclude parameters in the mounted hook, and then update (delete) the data of this.cache object in real time. The core of the pruneCache function is to call the pruneCacheEntry
function pruneCache (keepAliveInstance: any, filter: Function) { const { cache, keys, _vnode } = keepAliveInstance for (const key in cache) { const cachedNode: ?VNode = cache[key] if (cachedNode) { const name: ?string = getComponentName(cachedNode.componentOptions) if (name && !filter(name)) { pruneCacheEntry(cache, key, keys, _vnode) } } } }
Author: amCow
Link: https://www.jianshu.com/p/9523bb439950
Source: Jianshu
The copyright belongs to the author. For commercial reprint, please contact the author for authorization, and for non-commercial reprint, please indicate the source.
- render
render () { const slot = this.$slots.defalut const vnode: VNode = getFirstComponentChild(slot) // The first child component object was found const componentOptions : ?VNodeComponentOptions = vnode && vnode.componentOptions if (componentOptions) { // Component parameters exist // check pattern const name: ?string = getComponentName(componentOptions) // Component name const { include, exclude } = this if (// Condition matching // not included (include && (!name || !matches(include, name)))|| // excluded (exclude && name && matches(exclude, name)) ) { return vnode } const { cache, keys } = this // Define the cache key of the component const key: ?string = vnode.key === null ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key if (cache[key]) { // The component has already been cached vnode.componentInstance = cache[key].componentInstance remove(keys, key) keys.push(key) // Adjust key sorting } else { cache[key] = vnode //Cache component object keys.push(key) if (this.max && keys.length > parseInt(this.max)) { //Cache limit exceeded, the first one will be deleted pruneCacheEntry(cahce, keys[0], keys, this._vnode) } } vnode.data.keepAlive = true //It is necessary to render and execute the hook function of the wrapped component } return vnode || (slot && slot[0]) }
- Step 1: get the first sub component object wrapped by keep alive and its component name;
- Step 2: match the conditions according to the set black-and-white list (if any) to decide whether to cache. If there is no match, directly return the component instance (VNode), otherwise execute step 3;
- Step 3: generate the cache key according to the component ID and tag, and find out whether the component instance has been cached in the cache object. If it exists, directly take out the cache value and update the position of the key in this.keys (updating the position of the key is the key to realizing the LRU replacement strategy), otherwise execute step 4;
- Step 4: store the component instance in this.cache object and save the key value. Then check whether the number of cached instances exceeds the max setting. If it exceeds the max setting, delete the most recently unused instance (that is, the key with subscript 0) according to the LRU replacement policy;
- Step 5: finally and importantly, set the keepAlive property value of the component instance to true.
4, Play: rendering
4.1 rendering process of Vue
See the whole process of Vue rendering with the help of a figure:
vue render pass.png : Keep alive implementation principle - brief book
The rendering of Vue starts from the render stage in the figure, but the rendering of keep alive is in the patch stage, which is the process of building a component tree (virtual DOM tree) and converting VNode into a real DOM node.
Briefly describe the process from render to patch
Let's start with the simplest new Vue:
import App from './App.vue' new Vue({ render: h => h(App) }).$mount('#app')
- Vue calls the on the prototype first when rendering_ The render function converts the component object into a VNode instance; And_ Render is converted by calling createElement and createEmptyVNode functions;
- The conversion process of createElement will select new VNode or call createComponent function to instantiate VNode according to different situations;
- After VNode instantiation is completed, Vue calls the on the prototype_ The update function renders VNode into a real DOM, and this process is completed by calling the patch function (this is the patch stage)
Represented by a diagram:-
Render pass.png
-
4.2 rendering of keep alive components
We all know that keep alive does not generate real DOM nodes. How does this work?
// src/core/instance/lifecycle.js export function initLifecycle (vm: Component) { const options= vm.$options // The first non abstract parent component instance was found let parent = options.parent if (parent && !options.abstract) { while (parent.$options.abstract && parent.$parent) { parent = parent.$parent } parent.$children.push(vm) } vm.$parent = parent // ... }
When Vue initializes the life cycle, it establishes a parent-child relationship for the component instance, and determines whether to ignore a component according to the abstract attribute. In keep alive, if abstract:true is set, Vue will skip the component instance.
Finally, the built component tree will not contain keep alive components, so the DOM tree rendered by the component tree will not have keep alive related nodes.
How does the component of keep alive package use cache?
In the patch phase, the createComponent function is executed:
// 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) } if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) insert(parentElem, vnode.elem, refElem) // Insert the cached DOM(vnode.elem) into the parent element if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentEle, refElm) } return true } } }
- When the wrapped component is loaded for the first time, it can be seen from the render function in keep-alive.js that the value of vnode.componentInstance is unfined and the value of keep alive is true. Because keep alive component is the parent component, its render function will be executed before the wrapped component; Then only i(vnode,false) is executed, and the following logic is not executed;
- When accessing the wrapped component again, the value of vnode.componentInstance is the cached component instance. Then insert(parentElm, vnode.elm, refElm) logic will be executed, so that the last DOM will be directly inserted into the parent element.
5, Not to be ignored: hook function
5.1 hook executed only once
For general components, each load will have a complete life cycle, that is, the hook function in the life cycle will be triggered. Why is the component wrapped by keep alive not?
The cached component instance will set keepAlive= true for it. In the initialization component hook function:
// src/core/vdom/create-component.js const componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ?boolean{ if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // keep-alive components, treat as a patch const mountedNode:any = vnode componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { const child = vnode.componentInstance = createComponentInstanceForVnode (vnode, activeInstance) } } }
It can be seen that when vnode.componentInstance and keepAlive are both true, the $mount process will not be entered, and all hook functions (beforeCreate, created and mounted) before mounted will not be executed.
5.2 repeatable activated
At the patch stage, the invokeInsertHook function will be executed at last, and this function is to call the insert hook of the component instance (VNode):
// src/core/vdom/patch.js function invokeInsertHook (vnode, queue, initial) { if (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data,pendingInsert = queue } else { for(let i =0; i<queue.length; ++i) { queue[i].data.hook.insert(queue[i]) // Call the insert hook function of VNode itself } } }
Look at the insert hook:
const componentVNodeHooks = { // init() insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance._isMounted) { componentInstance._isMounted = true callHook(componentInstance, 'mounted') } if (vnode.data.keepAlive) { if (context._isMounted) { queueActivatedComponent(componentInstance) } else { activateChildComponent(componentInstance, true/* direct */) } } // ... } }
In this hook, the activateChildComponent function is called to recursively execute the activated hook functions of all sub components:
// 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') } }
On the contrary, the deactivated hook function is the same principle, calling deactivateChildComponent function in the destroy hook function of the component instance (VNode).
reference resources
Author: amCow
Link: https://www.jianshu.com/p/9523bb439950
Source: Jianshu
The copyright belongs to the author. For commercial reprint, please contact the author for authorization, and for non-commercial reprint, please indicate the source.