手写一下LRU算法?知道在keepalive组件里是用到了吗?
什么是LRU算法?
var LRUCache = function (capacity) {
this.catch = new Map() //初始化map数据结构
this.capacity = capacity //容量
};
LRUCache.prototype.get = function (key) {
if (this.catch.has(key)) { //map中有这个元素
let value = this.catch.get(key); //调用map的get方法获取元素
//更新key=>value
this.catch.delete(key); //删除之前的元素
this.catch.set(key, value); //将新获取的相同的元素以键值对推入map中
return value //返回关键字的值
}
return -1 //map中没有这个元素返回-1
};
LRUCache.prototype.put = function (key, value) {
if (this.catch.has(key)) { //有这个元素
this.catch.delete(key); //删除
}
//判断有没有达到存储的阈值
if (this.catch.size >= this.capacity) {
//移除谁 再放新值
//m.keys().next()拿到首位的键值对
this.catch.delete(this.catch.keys().next().value)
}
this.catch.set(key, value);
};
//验证
let lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
keepalive的基本使用
keepalive是什么
keep-alive是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
keepailve的基本用法
- 动态组件中的应用,在线地址:官网的例子
<script setup>
import { shallowRef } from 'vue'
import CompA from './CompA.vue'
import CompB from './CompB.vue'
// 进行浅层代理,避免不必要的花销
const current = shallowRef(CompA)
</script>
<template>
<div class="demo">
<!--切换时,current.value值会发生改变 -->
<label><input type="radio" v-model="current" :value="CompA" /> A</label>
<label><input type="radio" v-model="current" :value="CompB" /> B</label>
<KeepAlive>
<component :is="current"></component>
</KeepAlive>
</div>
</template>
- 在vue-router中的应用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
<router-view></router-view>
</keep-alive>
include
定义缓存白名单,keep-alive会缓存命中的组件;exclude
定义缓存黑名单,被命中的组件将不会被缓存,优先级高于前者;max
定义缓存组件上限,超出上限使用LRU的策略置换缓存数据。
// 只缓存组件name为a或者b的组件
<keep-alive include="a,b">
<component />
</keep-alive>
// 组件name为c的组件不缓存(可以保留它的状态或避免重新渲染)
<keep-alive exclude="c">
<component />
</keep-alive>
// 如果同时使用include,exclude,那么exclude优先于include, 下面的例子只缓存a组件
<keep-alive include="a,b" exclude="b">
<component />
</keep-alive>
// 如果缓存的组件超过了max设定的值5,那么将删除第一个缓存的组件
<keep-alive exclude="c" max="5">
<component />
</keep-alive>
解析keepailve源码
keepailve的作用流程
Vue中的<keep-alive>
组件用于缓存和重用动态组件或组件树,以提高应用程序性能。它可以组件保留在内存中而不是每次重新渲时销毁和重新创建组。
下面是Vue中<keep-alive>
作用流程:
-
当一个
<keep-alive>
包裹组件第一次渲染时,该组件会被存起来,并且例会被保留在内存中。 -
当这个组件被切换出去例如,通过
v-if
或路由导离开了该组件),它并不会被销毁,而是早已经被放到一个名为cache
的缓存对象。 -
如果之后再次切换回这个组,它会从缓存中取,并重新插入到DOM中,而不是重新创建一个新的实例。
-
在组件被缓存期间,它的生命周期钩子函数不会被调用。但是,
activated
和deactivated
这两个特殊的生命周期钩子函数会在组件被激活和停用时被调用。 -
如果需要对缓存的件进行操作,可以使用
<keep-alive>
的特殊属性include
和exclude
来指定哪些组件需要被缓存或排除缓存之外。
总结来说<keep-alive>
的作用是将态组件或组树缓存起来,以避免复创建和销毁,提高应用程序的能。它通过在件切换时将组移入和移出缓来实现这一功能,并且可以通过特殊属性来控制哪些组件需要被存。
keep-alive.js
下面是源码里的整体架构:
export default {
name: 'keep-alive',
abstract: true, // 判断当前组件虚拟dom是否渲染成真是dom的关键
props: {
include: patternTypes, // 缓存白名单
exclude: patternTypes, // 缓存黑名单
max: [String, Number] // 缓存的组件实例数量上限
},
// 初始化
created () {
this.cache = Object.create(null) // 缓存虚拟dom
this.keys = [] // 缓存的虚拟dom的健集合
},
// 删除所有的缓存
destroyed () {
},
// 实时监听黑白名单的变动
mounted () {
},
// 渲染
render () {
}
}
keep-alive在它生命周期内定义了三个钩子函数:
- created 初始化,初始化两个对象分别缓存VNode(虚拟DOM)和VNode对应的键集合
- destroyed 删除
this.cache
中缓存的VNode实例,删除缓存VNode还要对应执行组件实例的destory
钩子函数。
destroyed () {
for (const key in this.cache) {
// 删除所有的缓存
pruneCacheEntry(this.cache, key, this.keys)
}
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() // 执行组件的destory钩子函数
}
cache[key] = null
remove(keys, key)
}
- 在
mounted
这个钩子中对include
和exclude
参数进行监听,然后实时地更新(删除)this.cache
对象数据。pruneCache
函数的核心也是去调用pruneCacheEntry
。
mounted () {
// 实时监听黑白名单的变动
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
- render
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot) // 找到第一个子组件对象
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) { // 存在组件参数
// check pattern
const name: ?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
const key: ?string = vnode.key == null // 定义组件的缓存key
// 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) // 调整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])
}
-
第一步:获取keep-alive包裹着的第一个子组件对象及其组件名;
-
第二步:根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步;
-
第三步:根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该
key
在this.keys
中的位置(更新key的位置是实现LRU置换策略的关键),否则执行第四步; -
第四步:在
this.cache
对象中存储该组件实例并保存key
值,之后检查缓存的实例数量是否超过max
的设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key)。 -
第五步:最后并且很重要,将该组件实例的
keepAlive
属性值设置为true
。这个在@不可忽视:钩子函数 章节会再次出场。
渲染时abstract
变量和keepAlive
变量的作用
到此为止,我们只了解了 <keep-alive>
的组件实现,但并不知道它包裹的子组件渲染和普通组件有什么不一样的地方。我们关注 2 个方面,首次渲染和缓存渲染。
同样为了更好地理解,我们也结合一个示例来分析:
let A = {
template: '<div class="a">' +
'<p>A Comp</p>' +
'</div>',
name: 'A'
}
let B = {
template: '<div class="b">' +
'<p>B Comp</p>' +
'</div>',
name: 'B'
}
let vm = new Vue({
el: '#app',
template: '<div>' +
'<keep-alive>' +
'<component :is="currentComp">' +
'</component>' +
'</keep-alive>' +
'<button @click="change">switch</button>' +
'</div>',
data: {
currentComp: 'A'
},
methods: {
change() {
this.currentComp = this.currentComp === 'A' ? 'B' : 'A'
}
},
components: {
A,
B
}
})
Vue在初始化生命周期的时候,为组件实例建立父子关系会根据abstract
属性决定是否忽略某个组件。在keep-alive中,设置了abstract: true
,那Vue就会跳过该组件实例。
// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
const options = vm.$options
// 找到第一个非abstract的父组件实例
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
// ...
}
我们知道 Vue 的渲染最后都会到 patch
过程,而组件的 patch
过程会执行 createComponent
方法,它的定义在 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
}
}
}
createComponent
定义了 isReactivated
的变量,它是根据 vnode.componentInstance
以及 vnode.data.keepAlive
的判断,第一次渲染的时候,vnode.componentInstance
为 undefined
,vnode.data.keepAlive
为 true,因为它的父组件 <keep-alive>
的 render
函数会先执行,那么该 vnode
缓存到内存中,并且设置 vnode.data.keepAlive
为 true,因此 isReactivated
为 false
,那么走正常的 init
的钩子函数执行组件的 mount
。当 vnode
已经执行完 patch
后,执行 initComponent
函数:
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
这里会有 vnode.elm
缓存了 vnode
创建生成的 DOM 节点。所以对于首次渲染而言,除了在 <keep-alive>
中建立缓存,和普通组件渲染没什么区别。
所以对我们的例子,初始化渲染 A
组件以及第一次点击 switch
渲染 B
组件,都是首次渲染。
简单来说:设置了abstract: true
,那Vue就会跳过该组件实例进行构建虚拟DOM,keepAlive
的值是true
,那么后续是通过将缓存插入到DOM里实现构建的。
缓存实例的生命周期
在初始化组件钩子函数中:
// 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)
}
}
// ...
}
可以看出,当vnode.componentInstance
和keepAlive
同时为truly值时,不再进入$mount
过程,那mounted
之前的所有钩子函数(beforeCreate
、created
、mounted
)都不再执行。
当一个组件实例从 DOM 上移除但因为被 <KeepAlive>
缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活。
在patch
的阶段,那么可以通过 onActivated()
和 onDeactivated()
注册相应的两个状态的生命周期钩子:
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
})
onDeactivated(() => {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
})
</script>
请注意:
onActivated
在组件挂载时也会调用,并且onDeactivated
在组件卸载时也会调用。- 这两个钩子不仅适用于
<KeepAlive>
缓存的根组件,也适用于缓存树中的后代组件。
结
参考:
转载自:https://juejin.cn/post/7257873564958785591