生命周期
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的提问。
有需要的小伙伴,可以点击下方卡片领取,无偿分享