生命周期
keep-alive 包裹的子组件在再次渲染时,并不会执行 mounted 生命周期钩子,只会执行 activated 钩子。而当子组件被掩藏时,也不会执行 destroyed 生命周期钩子,而是执行 deactivated 钩子
假设现在 keep-alive 包裹的动态组件中,可以在 child1 和 child2 两个组件之间进行切换,那么当从 child1 切换到 child2 时, child1 组件会执行 deactivated 钩子, 当从 child2 再次切回 child1 时,会执行 child2 的 deactivated ,然后执行 child1 的 activated 钩子
deactivated
先从组件销毁说起,当从 child1 切换到 child2 时, child1 会执行 deactivated 钩子而不是 destroyed 钩子。在前面分析 patch 过程中会对新旧节点的改变进行对比,从而尽可能范围小的去操作真实 DOM ,当 diff 完成对节点操作之后,接下来还有一个重要的步骤就是对旧的组件执行销毁移除操作。
function patch (oldVnode, vnode, hydrating, removeOnly) {// destroy old nodeif (isDef(parentElm)) {removeVnodes(parentElm, [oldVnode], 0, 0)} else if (isDef(oldVnode.tag)) {invokeDestroyHook(oldVnode)}
}
function removeVnodes (parentElm, vnodes, startIdx, endIdx) {for (; startIdx <= endIdx; ++startIdx) {// 拿到需要移除的组件const ch = vnodes[startIdx]if (isDef(ch)) {if (isDef(ch.tag)) {// 真实节点的移除操作removeAndInvokeRemoveHook(ch)invokeDestroyHook(ch)} else { // Text noderemoveNode(ch.elm)}}}
}
removeAndInvokeRemoveHook 会对旧的节点进行移除操作,其中关键的一步就是会将真实节点从父元素中删除。 invokeDestroyHook 是销毁组件钩子的核心,如果该组件下存在子组件,会递归去调用 invokeDestroyHook 执行销毁操作。销毁过程会执行组件内部定义的 destroy 钩子
function invokeDestroyHook (vnode) {let i, jconst data = vnode.dataif (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)// 执行组件内部的 destroy 钩子函数for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)}// 如果组件存在子组件,则遍历子组件递归调用 invokeDestroyHook 执行钩子if (isDef(i = vnode.children)) {for (j = 0; j < vnode.children.length; ++j) {invokeDestroyHook(vnode.children[j])}}
}
前面已经分析了组件 init prePatch 两个内部钩子,接下来看看 destroy 钩子函数
const componentVNodeHooks = {destroy (vnode: MountedComponentVNode) {// 获取到组件实例const { componentInstance } = vnodeif (!componentInstance._isDestroyed) {if (!vnode.data.keepAlive) {// 如果不是 keep-alive 组件,则执行销毁操作componentInstance.$destroy()} else {// 如果是已经缓存的组件deactivateChildComponent(componentInstance, true /* direct */)}}}
}
当组件是 keep-alive 缓存过的组件时, 即用 keepAlive 标记过的组件,则不会执行实例的销毁了,即 componentInstance.$destroy() 过程。 $destroy() 过程会做一些列的组件销毁操作,其中 beforeDestroy 和 destroyed 钩子函数也是在 $destroy 过程中进行调用。而 deactivateChildComponent 的处理过程则和 $destroy 完全不同。
export function deactivateChildComponent (vm: Component, direct?: boolean) {if (direct) {vm._directInactive = trueif (isInInactiveTree(vm)) {return}}if (!vm._inactive) {// 标记组件已经被停用vm._inactive = truefor (let i = 0; i < vm.$children.length; i++) {// 存在子组件时,递归调用。deactivateChildComponent(vm.$children[i])}// 调用 deactivated 钩子callHook(vm, 'deactivated')}
}
_directInactive 用来标记这个被停用的组件是否时最顶层的组件。而 _inactive 是停用的标记,同样子组件也需要递归去调用 deactivateChildComponent ,打上停用标记。最终会执行用户定义的 deactivated 钩子函数
activated
同样是在 patch 过程中, 当旧组件移除并销毁或停用之后,对新的组件也会执行相应的钩子。这也是停用的钩子会比启用的钩子先执行的原因。
function patch (oldVnode, vnode, hydrating, removeOnly) {{// 销毁或停用节点if (isDef(parentElm)) {removeVnodes(parentElm, [oldVnode], 0, 0)} else if (isDef(oldVnode.tag)) {invokeDestroyHook(oldVnode)}}// 插入节点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// 当节点已经被插入是,会延迟执行 insert 钩子if (isTrue(initial) && isDef(vnode.parent)) {vnode.parent.data.pendingInsert = queue} else {for (let i = 0; i < queue.length; ++i) {// 调用组件内部的 insert 钩子函数queue[i].data.hook.insert(queue[i])}}
}
来看下组件的 insert 钩子函数的具体实现
const componentVNodeHooks = {insert (vnode: MountedComponentVNode) {const { context, componentInstance } = vnodeif (!componentInstance._isMounted) {componentInstance._isMounted = truecallHook(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 */)}}},
}
在第一次实例化组件时,由于组件的 _isMounted 属性不存在,所以会调用 mounted 钩子函数,当从 child2 再次切回 child1 时,由于 child1 只是被停用而没有被销毁,所以不会再次调用 mounted 钩子函数,此时会执行 activateChildComponent 函数对组件的状态进行处理, activateChildComponent 和前面分析过的 deactivateChildComponent 方法类似,都是对组件的启用状态进行更新处理。
export function activateChildComponent (vm: Component, direct?: boolean) {if (direct) {vm._directInactive = falseif (isInInactiveTree(vm)) {return}} else if (vm._directInactive) {return}if (vm._inactive || vm._inactive === null) {// 标记组件处于启用状态vm._inactive = falsefor (let i = 0; i < vm.$children.length; i++) {// 递归处理子组件的启用状态。activateChildComponent(vm.$children[i])}callHook(vm, 'activated')}
}
缓存优化 - LRU 算法
程序的内存空间使用有限的,所以我们无法无节制的对数据进行储存,这需要有策略去淘汰不那么重要的数据,保持最大数据存储量的一致。
根据淘汰机制的不同,常用有以下三种:
- 1.FIFO:先进先出策略 通过记录数据使用使用的使用,当缓存大小即将溢出时,优先清除里当前时间最远的数据
- 2.LRU:最近最少使用
LRU策略遵循的原则时, 如果数据最近被使用(访问)过,那么认为将来被访问的概率会更改。如果使用一个数组去记录数据,当一段数据被访问时,该数据会被移动到数组的末尾,表明该数据最近被使用过,当缓存溢出时,会删除数据头部的数据,即将最少使用的数据移除。> 对于LRU算法,个人认为翻译成最近最久未使用会贴切一点,在算法的实现过程中,并没有关注数据在指定时间段的使用次数,而是直接淘汰上一次使用时间距离当前时间最久的数据* 3.LFU: 计数最少策略 记录每一个数据的使用次数,当缓存溢出时,淘汰使用次数最少的数据。
上面三种缓存算法各有优劣,使用与不同的场景。而对于 Vue 中 keep-alive 在缓存组件时的优化处理,很明显利用了 LRU 的缓存策略,来看下关键代码
export default {// 渲染函数render () {if (cache[key]) {// 命中缓存,// make current key freshestremove(keys, key)keys.push(key)} else {// 初次渲染,缓存 vnodecache[key] = vnodekeys.push(key)// prune oldest entryif (this.max && 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)}}
}
每次执行 render 函数时,会先查找缓存是否存在对应组件的缓存,如果不存在缓存,则添加缓存,并将组件的 key 保存在 keys 数据末尾,表明该组件时 keep-alive 最近一次渲染的子组件。如果查找到缓存,则将组件的 key 从 keys 数组中删除,重新添加到数据末尾,每次都执行相同的操作。
当缓存的数组大小超过规定的大小之后,删除 keys 数组头部的数据,并删除对应 key 的缓存数据,因为 keys 头部 key 对应的组件就是最久未使用的组件数据,符合 LRU 算法策略。
最后
整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。




有需要的小伙伴,可以点击下方卡片领取,无偿分享
文章详细阐述了Vue.js中keep-alive组件如何管理子组件的激活与停用,通过deactivated和activated钩子实现缓存功能,避免重复渲染。同时,文章介绍了LRU算法在keep-alive中的应用,用于决定哪些组件应该被缓存,哪些应该被淘汰,以优化内存使用。

7727

被折叠的 条评论
为什么被折叠?



