keep-alive的实现原理
使用vue的时候,想必大家都是用过keep-alive,其作用就是缓存页面以及其状态。使用了这么久vue只知道如何使用但不明白其中原理,昨天翻看实现代码,这里做个笔记。
这里以vue3为例
整个组件的源码为:
const KeepAliveImpl = {
name: `KeepAlive`,
// Marker for special handling inside the renderer. We are not using a ===
// check directly on KeepAlive in the renderer, because importing it directly
// would prevent it from being tree-shaken.
__isKeepAlive: true,
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},
setup(props: KeepAliveProps, { slots }: SetupContext) {
const cache: Cache = new Map()
const keys: Keys = new Set()
let current: VNode | null = null
const instance = getCurrentInstance()!
// console.log('instance',instance)
// KeepAlive communicates with the instantiated renderer via the "sink"
// where the renderer passes in platform-specific functions, and the
// KeepAlive instance exposes activate/deactivate implementations.
// The whole point of this is to avoid importing KeepAlive directly in the
// renderer to facilitate tree-shaking.
const sink = instance.sink as KeepAliveSink
const {
renderer: {
move,
unmount: _unmount,
options: { createElement }
},
parentSuspense
} = sink
const storageContainer = createElement('div')
// console.log('sink',sink)
sink.activate = (vnode, container, anchor) => {
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
queuePostRenderEffect(() => {
const component = vnode.component!
component.isDeactivated = false
if (component.a !== null) {
invokeHooks(component.a)
}
}, parentSuspense)
}
sink.deactivate = (vnode: VNode) => {
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
queuePostRenderEffect(() => {
const component = vnode.component!
if (component.da !== null) {
invokeHooks(component.da)
}
component.isDeactivated = true
}, parentSuspense)
}
function unmount(vnode: VNode) {
// reset the shapeFlag so it can be properly unmounted
vnode.shapeFlag = ShapeFlags.STATEFUL_COMPONENT
_unmount(vnode, instance, parentSuspense)
}
function pruneCache(filter?: (name: string) => boolean) {
cache.forEach((vnode, key) => {
const name = getName(vnode.type as Component)
if (name && (!filter || !filter(name))) {
pruneCacheEntry(key)
}
})
}
function pruneCacheEntry(key: CacheKey) {
const cached = cache.get(key) as VNode
if (!current || cached.type !== current.type) {
unmount(cached)
} else if (current) {
// current active instance should no longer be kept-alive.
// we can't unmount it now but it might be later, so reset its flag now.
current.shapeFlag = ShapeFlags.STATEFUL_COMPONENT
}
cache.delete(key)
keys.delete(key)
}
watch(
() => [props.include, props.exclude],
([include, exclude]) => {
include && pruneCache(name => matches(include, name))
exclude && pruneCache(name => matches(exclude, name))
},
{ lazy: true }
)
onBeforeUnmount(() => {
cache.forEach(unmount)
})
return () => {
if (!slots.default) {
return null
}
const children = slots.default()
let vnode = children[0]
if (children.length > 1) {
if (__DEV__) {
warn(`KeepAlive should contain exactly one component child.`)
}
current = null
return children
} else if (
!isVNode(vnode) ||
!(vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT)
) {
current = null
return vnode
}
const comp = vnode.type as Component
const name = getName(comp)
const { include, exclude, max } = props
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
return vnode
}
const key = vnode.key == null ? comp : vnode.key
const cached = cache.get(key)
// clone vnode if it's reused because we are going to mutate it
if (vnode.el) {
vnode = cloneVNode(vnode)
}
cache.set(key, vnode)
if (cached) {
// copy over mounted state
vnode.el = cached.el
vnode.anchor = cached.anchor
vnode.component = cached.component
if (vnode.transition) {
// recursively update transition hooks on subTree
setTransitionHooks(vnode, vnode.transition!)
}
// avoid vnode being mounted as fresh
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// make this key the freshest
keys.delete(key)
keys.add(key)
} else {
keys.add(key)
// prune oldest entry
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(Array.from(keys)[0])
}
}
// avoid vnode being unmounted
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = vnode
return vnode
}
}
}
很容易看出keep-alive其实就是vue自己封装的一个组件,和普通组件一样。
再讲keep-alive组件前先了解下vue组件的整个渲染
大致流程如下
keep-alive生命周期
组件挂载:
调用setupStatefulComponent函数触发组件setup方法,其中组件的setup方法核心代码其实就几行:
return () => {
const children = slots.default()
let vnode = children[0]
cache.set(key, vnode)
if (cached) {
vnode.el = cached.el
vnode.anchor = cached.anchor
vnode.component = cached.component
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
keys.delete(key)
keys.add(key)
} else {
keys.add(key)
}
return vnode
}
主要逻辑为三:
1.确认需要渲染的slot、
2.将其状态置入缓存或读取已存在的缓存、
3.返回slot对应的vnode,紧接着调用setupRenderEffect,渲染出dom。
组件更新(slot变化):
当slot变化后,首先会调用keep-alive组件的render即setup的返回函数,逻辑见上面setup方法。紧接着当某个slot卸载时,会调用deactivate函数,当某个slot重新挂载时,则会调用activate函数,核心代码如下:
const storageContainer = createElement('div')
sink.activate = (vnode, container, anchor) => {
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
queuePostRenderEffect(() => {
const component = vnode.component!
component.isDeactivated = false
if (component.a !== null) {
invokeHooks(component.a)
}
}, parentSuspense)
}
sink.deactivate = (vnode: VNode) => {
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
queuePostRenderEffect(() => {
const component = vnode.component!
if (component.da !== null) {
invokeHooks(component.da)
}
component.isDeactivated = true
}, parentSuspense)
}
逻辑也很简单,当组件卸载时,将其移入缓存的dom节点中,调用slot的deactivate生命周期,当组件重新挂载时候,将其移入至挂载的dom节点中。
总结来说,keep-alive实现原理就是将对应的状态放入一个cache对象中,对应的dom节点放入缓存dom中,当下次再次需要渲染时,从对象中获取状态,从缓存dom中移出至挂载dom节点中。
keep-alive的使用总结
在平常开发中,有些组件只需要加载一次,后面的数据将不存在变化,亦或者是组件需要缓存状态,滚动条位置等,这个时候,keep-alive的用处就立刻凸显出来了。
1.App.vue中使用keep-alive
include表示需要缓存的页面,exclude表示不需要缓存的页面,你可以只设置其中一个即可,但两个同时设置的时候,切记exclude优先级高于include,例如a组件在exclude中和include中都存在,那么,a组件是不会被缓存的
<template>
<div id="app">
<keep-alive :include="whiteList" :exclude="blackList">
<router-view v-if="isRouterAlive" ></router-view>
</keep-alive>
</div>
</template>
<script>
export default {
name: 'App',
data(){
return{
isRouterAlive:true,
whiteList:['styleLibrary','OrderList','SalesData'],
blackList:['Footer'],
personShow:false,
}
},
}
</script>
2.App.vue中配合router进行使用
<template>
<div id="app">
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view> <!--缓存组件-->
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view> <!--非缓存组件-->
</div>
</template>
将需要缓存的组件的$route.meta中的keepAlive设置为true,反之为false
{
path:'/login',
name:'login',
component:resolve=>require(['@/pages/login'],resolve),
meta:{
keepAlive:true,
title:'登录',
savedPosition:true,
}
},
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。