1.使用场景:
1. 动态组件切换,当你选择了一篇文章,切换到 b 标签,然后再切换回 a,是不会继续展示你之前选择的文章的。这是因为你每次切换新标签的时候,Vue 都创建了一个新的 currentTabComponent 实例;
2. 当我们第一次进入列表页需要请求一下数据,当我从列表页进入详情页,详情页不缓存也需要请求下数据,然后返回列表页,这时候我们使用keep-alive来缓存组件,防止二次渲染,这样会大大的节省性能
3. 使用一些tab页频繁的切换,但是数据不会频繁的刷新
2.作用:
缓存组件内部状态,避免重新渲染=>是一个抽象组件,自身不会渲染一个DOM元素,也不会出现在父组件链中
3.用法:
- 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们
- 缓存所有路径匹配到的路由组件,包括路由组件里面的组件,
<!-- 基本 -->
<keep-alive>
<component :is="view"></component>
</keep-alive>
<!-- 多个条件判断的子组件 -->
<keep-alive>
<comp-a v-if="a > 1"></comp-a>
<comp-b v-else></comp-b>
</keep-alive>
<!— 路由 -->
<keep-alive>
<router-view></router-view>
</keep-alive>
// routes 配置
export default [
{
path: '/',
name: 'home',
component: Home,
meta: {
keepAlive: true // 需要被缓存
}
}, {
path: '/:id',
name: 'edit',
component: Edit,
meta: {
keepAlive: false // 不需要被缓存
}
}
]
// 多层嵌套路由会出现问题,不缓存
<keep-alive>
<router-view v-if="$">
<!-- 这里是会被缓存的视图组件,比如 Home! -->
</router-view>
</keep-alive>
<router-view v-if="!$">
<!-- 这里是不被缓存的视图组件,比如 Edit! -->
</router-view>
4.参数:缓存想要缓存的路由
- include:匹配的路由/组件会被缓存
- exclude:匹配的路由/组件不会被缓存
- max:最大缓存数
- 使用方法:
- 采用逗号分隔的字符串形式
- 正则形式,必须采用v-bind形式使用
- 数组形式,必须采用v-bind形式使用
5.匹配规则:
- 首先匹配组件的name选项
- 如果name选项不可用,则匹配它的局部注册名称(父组件components选项的键值)
- 匿名组件,不可匹配(路由组件没有name选项,并且没有注册的组件名)
- 只能匹配当前被包裹的组件,不能匹配更下面嵌套的子组件=>例如:只能匹配路由组件的name选项,不能匹配路由组件里面的嵌套组件name选项
- 不会在函数式组件中正常工作,因为他们没有缓存实例
- exclude的优先级>include
<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
<!— 缓存路由 -->
<keep-alive include='a'>
<router-view></router-view>
</keep-alive>
5.钩子函数
在被keep-alive包含的组件/路由里,多出了两个生命周期的钩子:activated和deactivated
在组件第一次渲染时会被调用,之后在每次缓存组件被激活时调用
// 第一次进入缓存路由/组件,在mounted后面,beforeRouteEnter守卫传给next的回调函数之前
beforeMount=> 如果你是从别的路由/组件进来(组件销毁destroyed/或离开缓存deactivated)=>mounted=> activated 进入缓存组件
=> 执行 beforeRouteEnter回调
// 因为组件被缓存了,再次进入缓存路由/组件时,不会触发这些钩子 beforeCreate create beforeMount mounted
组件销毁destroyed/或离开缓存deactivated => activated 进入当前缓存组件 => 执行beforeRouteEnter回调 ( 组件缓存或销毁,嵌套组件的销毁和缓存也在这里触发)
:组件被停用(离开路由)时调用
- 使用了keep-alive就不会调用beforeDestory和destroyed,因为组件没被销毁,被缓存起来了=>可以看作beforeDestory的替代,如果缓存了组件,要在组件销毁的时候做一些事件,可以放在这个钩子
组件内的离开当前路由钩子beforeRouteLeave => 路由前置守卫 beforeEach =>
全局后置钩子afterEach => deactivated 离开缓存组件 => activated 进入缓存组件(如果你进入的也是缓存路由)
// 如果离开的组件没有缓存的话 beforeDestroy会替换deactivated
// 如果进入的路由也没有缓存的话 全局后置钩子afterEach=>销毁 destroyed=> beforeCreate等
6.实现原理
// src/core/components/
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 () {
for (const key in this.cache) { // 删除所有的缓存
pruneCacheEntry(this.cache, key, this.keys) // 遍历调用pruneCacheEntry函数删除=>删除缓存VNode并执行对应组件实例的destory钩子函数
}
},
mounted () {
// 实时监听黑白名单的变动
this.$watch('include', val => {
pruneCache(this, name => matches(val, name)) // 实时更新/删除对象数据
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
// src/core/components/
render () {
const slot = this.$slots.default // 获取插槽
const vnode: VNode = getFirstComponentChild(slot) // 获取keep-alive包裹着的第一个子组件对象及其组件名
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) { // 存在组件参数
// 获取组件名
const name: ?string = getComponentName(componentOptions) // 组件名
const { include, exclude } = this // 解构对象赋值常量
if ( // 根据设定的黑白名单进行条件匹配,决定是否缓存,不匹配直接返回VNode
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
// 根据组件ID和tag生成缓存key,并在缓存对象中查找是否已缓存过该组件实例
const { cache, keys } = this
const key: ?string = vnode.key == null // 定义组件的缓存key
// 相同的钩子函数可能会被作为不同的组件,所以仅仅cid是不够的
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
// 如果存在该组件实例,直接取出缓存值并更新该key在中的位置(更新key的位置是实现LRU置换策略的关键)
if (cache[key]) { // 已经缓存过该组件
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key) // 调整key排序
} else {
// 在对象中存储该组件实例并保存key值
cache[key] = vnode // 缓存组件对象
keys.push(key)
// 检查缓存的实例数量是否超过max的设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key)
if (this.max && keys.length > parseInt(this.max)) { // 超过缓存数限制,将第一个删除
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
// 将该组件实例的keepAlive属性值设为true
vnode.data.keepAlive = true // 渲染和执行被包裹组件的钩子函数需要用到
}
return vnode || (slot && slot[0])
}
}
keep-alive组件的渲染=>不会生成真正的DOM节点
// src/core/instance/
export function initLifecycle (vm: Component) {
const options = vm.$options
// 找到第一个非abstract的父组件实例
let parent = options.parent
// 在keep-alive中,设置了abstract:true,Vue就会跳过该组件实例=>最后构建的组件树中就不会包含keep-alive组件,那么由组件树渲染的DOM树自然也不会有keep-alive相关的节点了
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
// ...
}
keep-alive包裹的组件使用缓存:在patch阶段中,会执行createConponent函数
// src/core/vdom/
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
// 在首次加载被包裹组件时,由keep-alive中的render函数可知,的值是undefined,keepAlive的值是true,因为keep-alive作为父组件,它的render函数会先于被包裹组件执行,那么就执行到i(vnode, false /* hydrating */),后面的逻辑就不再执行
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */) // 走正常的init钩子函数执行组件的mount
}
// 再次访问被包裹的组件时,的值就已经缓存的组件实例,那么会往下执行
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm) // 将缓存的DOM()插入父元素中
if (isTrue(isReactivated)) {
// reactivateComponent函数中会执行insert(parentElm, , refElm) 把缓存的 DOM 对象直接插入到目标元素中
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
不执行组件的created、mounted等钩子函数的原因:
// src/core/vdom/
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// 不再进入$mount过程,mounted之前的钩子函数(beforeCreate、created、mounted)都不再执行
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)
}
}
// ...
}
activated钩子函数=>执行时机是 包裹的组件渲染的时候
// src/core/vdom/
// 调用组件实例(VNode)自身的insert钩子函数
function invokeInsertHook (vnode, queue, initial) {
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i]) // 调用VNode自身的insert钩子函数
}
}
}
// src/core/vdom/
const componentVNodeHooks = {
// init()
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
// 判断<keep-alive>包裹的组件是否已经mounted
if (context._isMounted) {
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
// ...
}
// src/core/instance/
export function activateChildComponent (vm: Component, direct?: boolean) {
if (direct) {
vm._directInactive = false
if (isInInactiveTree(vm)) {
return
}
} else if (vm._directInactive) {
return
}
// 递归地执行它的所有子组件的activated钩子函数
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
callHook(vm, 'activated')
}
}
7.常用场景及方法:
1.切换tab,进行缓存,但又希望可以刷新数据
1.给用户机会触发刷新,增加下拉加载刷新事件
2.将获取数据的操作写在activated步骤
2.前进刷新,后退缓存用户浏览数据
搜索页面==>到搜索结果页时,搜索结果页面要重新获取数据,
搜索结果页面==>点击进入详情页==>从详情页返回列表页时,要保存上次已经加载的数据和自动还原上次的浏览位置。
<keep-alive>
<router-view v-if="$"/>
</keep-alive>
<router-view v-if="!$"/>
// list是我们的搜索结果页面
//
{
path: '/list',
name: 'List',
component: List,
meta: {
isUseCache: false, // 默认不缓存
keepAlive: true // 是否使用 keep-alive
}
}
// list组件的activated钩子
activated() {
//isUseCache为false时才重新刷新获取数据
//因为对list使用keep-alive来缓存组件,所以默认是会使用缓存数据的
if(!this.$route.meta.isUseCache){
this.list = []; // 清空原有数据
this.onLoad(); // 这是我们获取数据的函数
}
this.$route.meta.isUseCache = false // 通过这个控制刷新
},
// list组件的beforeRouteLeave钩子函数
// 跳转到详情页时,设置需要缓存 => beforeRouterLeave:离开当前路由时 => 导航在离开该组件的对应路由时调用,可以访问组件实例this=>用来禁止用户离开,比如还未保存草稿,或者在用户离开前把定时器销毁
beforeRouteLeave(to, from, next){
if(to.name=='Detail'){
from.meta.isUseCache = true
}
next()
}
3.前进刷新,后退缓存用户浏览数据
事件绑定了很多次,比如上传点击input监听change事件,突然显示了多张相同图片的问题
也就是说,DOM在编译后就缓存在内容中了,如果再次进入还再进行事件绑定初始化则就会发生这个问题
解决办法: 在mounted中绑定事件,因为这个只执行一次,并且DOM已准备好。如果插件绑定后还要再执行一下事件的handler函数的话,那就提取出来,放在activated中执行。比如:根据输入内容自动增长textarea的高度,这部分需要监听textarea的input和change事件,并且页面进入后还要再次执行一次handler函数,更新textarea高度(避免上次输入的影响)。
。
转载于 /zn740395858/article/details/90141539