Start the growth journey of Nuggets! This is the 17th day of my participation in the “Nuggets Daily New Project · February Update Challenge”, click to view the event details
Foreword
It is recommended to read two chapters before reading this chapter
[Vue source code] logic analysis of keep-alive components (Part 1)
[Vue source code] logic analysis of keep-alive components (in)
This chapter mainly analyzes the life cycle of the keep-alive
component, including the creation, destruction, mounting, etc. of the component; in addition, the keep-alive
used A brief analysis of the obtained cache algorithm, so as to have a certain understanding of the optimization strategy of keep-alive
in memory usage
Lifecycle
When subcomponents wrapped by keep-alive
are rendered again, the mounted
lifecycle hook will not be executed, only the activated
hook will be executed. And when the subcomponent is hidden, the destroyed
life cycle hook will not be executed, but the deactivated
hook will be executed
Assuming that in the dynamic component wrapped by keep-alive
, it is possible to switch between child1
and child2
, then when starting from child1
switches to child2
, the child1
component will execute the deactivated
hook, and when it is switched from child2
again When switching back to child1
, deactivated
of child2
will be executed, and then activated
of child1
will be executed > hook
deactivated
Let’s start with component destruction, when switching from child1
to child2
, child1
will execute the deactivated
hook instead of destroyed
hook. In the previous analysis of patch
, the changes of the old and new nodes will be compared, so as to operate the real DOM
as small as possible. When diff
is completed After operating the node, the next important step is to destroy and remove the old components.
function patch (oldVnode, vnode, hydrating, removeOnly) {<!-- --> // destroy old node if (isDef(parentElm)) {<!-- --> removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode. tag)) {<!-- --> invokeDestroyHook(oldVnode) } } function removeVnodes (parentElm, vnodes, startIdx, endIdx) {<!-- --> for (; startIdx <= endIdx; + + startIdx) {<!-- --> // Get the component that needs to be removed const ch = vnodes[startIdx] if (isDef(ch)) {<!-- --> if (isDef(ch. tag)) {<!-- --> // remove operation of real node removeAndInvokeRemoveHook(ch) invokeDestroyHook(ch) } else {<!-- --> // Text node removeNode(ch.elm) } } } }
removeAndInvokeRemoveHook
will remove the old node, and the key step is to delete the real node from the parent element. invokeDestroyHook
is the core of destroying component hooks. If there are subcomponents under this component, invokeDestroyHook
will be called recursively to perform the destruction operation. The destruction process will execute the destroy
hook defined inside the component
function invokeDestroyHook (vnode) {<!-- --> let i, j const data = vnode.data if (isDef(data)) {<!-- --> if (isDef(i = data.hook) & amp; & amp; isDef(i = i.destroy)) i(vnode) // Execute the destroy hook function inside the component for (i = 0; i < cbs.destroy.length; + + i) cbs.destroy[i](vnode) } // If the component has subcomponents, traverse the subcomponents and call invokeDestroyHook recursively to execute the hook if (isDef(i = vnode.children)) {<!-- --> for (j = 0; j < vnode.children.length; + + j) {<!-- --> invokeDestroyHook(vnode. children[j]) } } }
The two internal hooks of the component init
prePatch
have been analyzed before, let’s take a look at the destroy
hook function
const componentVNodeHooks = {<!-- --> destroy (vnode: MountedComponentVNode) {<!-- --> // Get the component instance const {<!-- --> componentInstance } = vnode if (!componentInstance._isDestroyed) {<!-- --> if (!vnode.data.keepAlive) {<!-- --> // If it is not a keep-alive component, execute the destroy operation componentInstance. $destroy() } else {<!-- --> // If it is a cached component deactivateChildComponent(componentInstance, true /* direct */) } } } }
When the component is a component cached by keep-alive
, that is, a component marked with keepAlive
, the instance will not be destroyed, that is, componentInstance.$ destroy()
procedure. The $destroy()
process will do a series of component destruction operations, and the beforeDestroy
and destroyed
hook functions are also in $destroy
code> is called. The process of deactivateChildComponent
is completely different from $destroy
.
export function deactivateChildComponent (vm: Component, direct?: boolean) {<!-- --> if (direct) {<!-- --> vm._directInactive = true if (isInInactiveTree(vm)) {<!-- --> return } } if (!vm._inactive) {<!-- --> // mark the component as disabled vm._inactive = true for (let i = 0; i < vm.$children.length; i ++ ) {<!-- --> // Called recursively when subcomponents exist. deactivateChildComponent(vm. $children[i]) } // call the deactivated hook callHook(vm, 'deactivated') } }
_directInactive
is used to mark whether the disabled component is the topmost component. And _inactive
is a deactivated mark, and the child component also needs to recursively call deactivateChildComponent
to mark it as deactivated. Finally, the user-defined deactivated
hook function will be executed
activated
Also in the patch
process, when the old component is removed and destroyed or deactivated, the corresponding hook will be executed for the new component. This is why disabled hooks are executed before enabled hooks.
function patch (oldVnode, vnode, hydrating, removeOnly) {<!-- --> {<!-- --> // destroy or deactivate the node if (isDef(parentElm)) {<!-- --> removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode. tag)) {<!-- --> invokeDestroyHook(oldVnode) } } // insert node invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } function invokeInsertHook (vnode, queue, initial) {<!-- --> // delay insert hooks for component root nodes, invoke them after the // element is really inserted // When the node has been inserted, the execution of the insert hook will be delayed if (isTrue(initial) & amp; & amp; isDef(vnode. parent)) {<!-- --> vnode.parent.data.pendingInsert = queue } else {<!-- --> for (let i = 0; i < queue. length; + + i) {<!-- --> // Call the insert hook function inside the component queue[i].data.hook.insert(queue[i]) } } }
Let’s look at the specific implementation of the component’s insert
hook function
const componentVNodeHooks = {<!-- --> insert (vnode: MountedComponentVNode) {<!-- --> const {<!-- --> context, componentInstance } = vnode 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 */) } } }, }
When the component is instantiated for the first time, because the _isMounted
property of the component does not exist, the mounted
hook function will be called, when the child2
is cut again When returning to child1
, since child1
is only deactivated but not destroyed, the mounted
hook function will not be called again, and the mounted
hook function will be executed at this time. The code>activateChildComponent function processes the state of the component. activateChildComponent
is similar to the previously analyzed deactivateChildComponent
method, which updates the activation state of the component.
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) {<!-- --> // mark component is enabled vm._inactive = false for (let i = 0; i < vm.$children.length; i ++ ) {<!-- --> // Recursively process the enabled state of child components. activateChildComponent(vm. $children[i]) } callHook(vm, 'activated') } }
Cache optimization – LRU algorithm
The memory space of the program is limited, so we cannot store data without restraint. This requires a strategy to eliminate less important data and keep the maximum data storage consistent.
According to different elimination mechanisms, the following three types are commonly used:
-
- FIFO: first-in-first-out policy By recording the usage of data usage, when the buffer size is about to overflow, the data with the farthest current time in the cache will be cleared first
-
- LRU: Least Recently Used When the
LRU
policy follows the principle, if the data has been used (accessed) recently, then the probability of being accessed in the future is considered to change. If an array is used to record data, when a piece of data is accessed, the data will be moved to the end of the array, indicating that the data has been used recently. When the buffer overflows, the data at the head of the data will be deleted, that is, the least used Data removal.
For the
LRU
algorithm, I personally think that it would be more appropriate to translate it into the most recently unused. In the implementation of the algorithm, it does not pay attention to the number of times the data is used in the specified time period, but directly eliminates the last usage time The oldest data from the current time - LRU: Least Recently Used When the
-
- LFU: Least count strategy records the number of times each data is used, and when the buffer overflows, the data with the least number of times is eliminated.
The above three cache algorithms have their own advantages and disadvantages, and they are used in different scenarios. As for the optimization of keep-alive
in Vue
when caching components, it is obvious that the cache strategy of LRU
is used. Let’s take a look at the key code
export default {<!-- --> // render function render () {<!-- --> if (cache[key]) {<!-- --> // Hit the cache, // make current key freshest remove(keys, key) keys. push(key) } else {<!-- --> // first render, cache vnode cache[key] = vnode keys. push(key) // prune oldest entry if (this.max & amp; & amp; keys.length > parseInt(this.max)) {<!-- --> pruneCacheEntry(cache, keys[0], keys, this._vnode) } } } } export function remove (arr: Array<any>, item: any): Array<any> | void {<!-- --> if (arr. length) {<!-- --> const index = arr. indexOf(item) if (index > -1) {<!-- --> return arr. splice(index, 1) } } }
Every time the render
function is executed, it will first check whether there is a cache for the corresponding component in the cache. If there is no cache, add the cache and save the key
of the component in keys
At the end of the data, it indicates that this component is the subcomponent of keep-alive
rendered last time. If the cache is found, remove the component’s key
from the keys
array, add it back to the end of the data, and perform the same operation each time.
When the size of the cached array exceeds the specified size, delete the data in the head of the keys
array and delete the cached data corresponding to key
, because keys
The component corresponding to the key
in the header is the component data that has not been used for a long time, which conforms to the LRU
algorithm strategy.