本文正在参加「金石计划 . 瓜分6万现金大奖」
前言
- 常网IT戳我呀!
- 常网IT源码上线啦!
- 本篇录入吊打面试官专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。
- 这段时间面了很多家公司,被问到的题我感觉不重复不止100道,将会挑选觉得常见且有意义的题目进行分析及回答。
- 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。
- 请问Vue中的keep-alive怎么缓存组件的,缓存后又是怎么更新的,背后的原理你造吗?
这两天屁股感觉有点微疼,这不周六就去看了一下,说是痔疮,没想到也中招了。
说手术只会让你长的这块地方不会长,但会从别处长起,不会消失,只会转移。
都说十人九人会,我是没机会做那其中的一人了。
避免久坐,多摸鱼才能有好转。
若面试着急用,最后面有回答示例~
一、问题剖析
keep-alive怎么缓存组件的,缓存后又是怎么更新的?
其实这个问题在项目实战中很常见,因为keep-alive
可以很好的缓存我们的组件,是性能优化常见的一点,面试中我们怎么去回答比较好✍,我个人觉得可以从以下四个方面去回答这个面试题。
- 缓存可用keep-alive,说一下它的作用与用法
- 使用细节,如缓存指定/排除、结合router和transition
- 组件缓存后更新可以利用activated或者beforeRouteEnter
- 原理阐述
二、回
第一
开发中缓存组件使用keep-alive组件,keep-alive是vue内置组件,keep-alive包裹动态组件component
时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
用法如下:
<keep-alive>
<component :is="view"></component>
</keep-alive>
第二
结合属性include
和exclude
可以明确指定缓存哪些组件或排除缓存指定组件。vue3中结合vue-router时变化较大,之前是keep-alive包裹router-view,现在需要反过来用router-view包裹keep-alive。
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component"></component>
</keep-alive>
</router-view>
keep-alive的中缓存的时候还运用了LRU
(Least Recently Used)算法。
面试官有一次问我,Vue中用到哪些算法?
在这里就可以说:在keep-alive中,他底层实现用到LRU算法进行组件的缓存机制。
我这里给大家分享一个我简历的一行:
使用LRU算法实现对应接口缓存,减少请求量。
大家如果感兴趣简历的,可以留言,后期看看出一篇简历中项目难点怎么写。
LRU
算法详细实现在文章的最后。(大家看完,可以运用到项目中、简历中💖)
好啦,我们继续往下回答正题。
第三
缓存后如果要获取数据,解决方案可以有以下两种:
beforeRouteEnter
在有vue-router的项目,每次进入路由的时候,都会执行beforeRouteEnter。
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
}
actived
在keep-alive缓存的组件被激活的时候,都会执行actived钩子。
activated(){
this.getData() // 获取数据
}
第四
keep-alive是一个通用组件,它内部定义了一个map
,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode
,如果该组件在map中存在就直接返回它。由于component的is属性是个响应式数据,因此只要它变化,keep-alive的render
函数就会重新执行。
面试官心里想:哎哟,细狗,回答得真细,有点意思。
追问:说说keep-alive的原理?
小伙子还不错,基底还是可以的,让我继续看看你的深根扎实不。
其实,上面回答之后,这个问题就告一段落了,但也有面试官抓着问题不放,继续追问下去。
🙋那你说说keep-alive的原理是什么?
面试官说:不把你难倒,我就对不起我刁难官的称号。
场景
回答原理之前,我们先来看看一个场景。
可能大家在平时的开发中会经常遇到这样的场景:有一个可以进行筛选的列表页List.vue,点击某一项时进入相应的详情页面,等到你从详情页返回List.vue时,发现列表页居然刷新了!刚刚的筛选条件都没了!!!
双十一来了,第一就想到某宝,搜索宝贝列表,进入详情页,回退的操作。
keep-alive是什么?
-
keep-alive本身不会渲染出来,也不会出现在父组件链中
-
keep-alive包裹动态组件时,会缓存不活动的组件,而不是销毁它们
-
缓存组件,可提升性能(比如某宝的宝贝,进入详情页,每次都是同一个物品,那不需要请求接口,直接缓存组件,当然,如果不是同一个就调接口)
怎么用?
知己知彼百战百胜。
知道keep-alive的用法,我们才能更好的去设计这个缓存组件的规则。
参数
keep-alive接收三个参数:
-
include
:可传字符串、正则表达式、数组,名称匹配成功的组件会被缓存 -
exclude
:可传字符串、正则表达式、数组,名称匹配成功的组件不会被缓存 -
max
:可传数字,限制缓存组件的最大数量,超过max则按照LRU算法进行置换
include和exclude,传数组情况居多。
生命周期
生命周期有:activated
激活、deactivated
离开
- activated: 页面第一次进入的时候
钩子触发的顺序是created->mounted->activated
- deactivated: 页面退出的时候会触发deactivated
当再次前进或者后退的时候只触发activated。
使用keep-alive
会将数据保留在内存中,如果要在每次进入页面的时候获取最新的数据,需要在activated阶段获取数据,承担原来created
钩子中获取数据的任务。
那么,我们一般会在动态组件、路由组件去用到keep-alive组件。
动态组件
<keep-alive :include="allowList" :exclude="noAllowList" :max="amount">
<component :is="currentComponent"></component>
</keep-alive>
路由组件
<keep-alive :include="allowList" :exclude="noAllowList" :max="amount">
<router-view></router-view>
</keep-alive>
底层原理
暴风雨来临的前兆,总是很安静。
突然就哼起:
给你的爱一直很安静,来交换你偶尔给的关心。
明明是三个人的电影,我却始终不能有姓名。
各位靓仔知道的,我不是真的想唱歌,主要是给下面沉重的知识活跃气氛。
先说一下,keep-alive在各个生命周期里都做了啥吧。
-
created
:初始化一个cache、keys
,cache用来存缓存组件的虚拟dom集合,keys用来存缓存组件的key集合。 -
mounted
:实时监听include、exclude这两个的变化,并执行相应操作。 -
destroyed
:删除掉所有缓存相关的东西。
之前说了,keep-alive不会被渲染到页面上,所以abstract这个属性至关重要!
keep-alive源码
// src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true, // 判断此组件是否需要在渲染成真实DOM
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created() {
this.cache = Object.create(null) // 创建对象来存储 缓存虚拟dom
this.keys = [] // 创建数组来存储 缓存key
},
mounted() {
// 实时监听include、exclude的变动
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
destroyed() {
for (const key in this.cache) { // 删除所有的缓存
pruneCacheEntry(this.cache, key, this.keys)
}
},
render() {
// 下面讲
}
}
上段代码,结合前面分析的:keep-alive在各个生命周期里都做了啥。不难理解😁。
pruneCacheEntry函数
上面keep-alive源码中,在destroyed
销毁生命周期中for循环执行pruneCacheEntry函数,看看该函数内部做了什么?
// src/core/components/keep-alive.js
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 // 设为null
remove(keys, key) // 删除对应的元素
}
- 1:遍历集合,执行所有缓存组件的$destroy方法
- 2:将cache对应key的内容设置为null
- 3:删除keys中对应的元素
render函数
上面keep-alive源码中,在最后保留了render空函数,我们在这里分析。
以下称include
为白名单,exclude
为黑名单。
render
函数里主要做了这些事:
- 第一步:获取到keep-alive包裹的第一个组件以及它的组件名称
- 第二步:判断此组件名称是否能被白名单、黑名单匹配,如果不能被白名单匹配 || 能被黑名单匹配,则直接返回
VNode
,不往下执行,如果不符合,则往下执行第三步 - 第三步:根据组件
ID、tag
生成缓存key,并在缓存集合中查找是否已缓存过此组件。如果已缓存过,直接取出缓存组件,并更新缓存key在keys中的位置(这是LRU算法的关键),如果没缓存过,则继续第四步 - 第四步:分别在
cache、keys
中保存此组件以及他的缓存key,并检查数量是否超过max
,超过则根据LRU算法进行删除 - 第五步:将此组件实例的keepAlive属性设置为true,这很重要哦,下面会讲到的!
再详细分析:
在第二步的时候,只要不能被白名单匹配或者能被黑名单匹配满足其实一个,那我们就不对该组件进行缓存,直接返回他的VNode
。
白名单就是我们需要缓存的组件,黑名单就是我们不需要缓存的组件。
认真看流程,可以理解的。(请再多看一遍)
渲染
咱们先来看看Vue一个组件是怎么渲染的,咱们从render开始说:
-
render
:此函数会将组件转成VNode
-
patch
:此函数在初次渲染时会直接渲染根据拿到的VNode
直接渲染成真实DOM,第二次渲染开始就会拿VNode
会跟旧VNode对比,打补丁(diff算法对比发生在此阶段),然后渲染成真实DOM
new Vue阶段图解(可以看看这篇面试官问我new Vue阶段做了什么?):包括咱这里提到的渲染。
patch
阶段会涉及到diff
算法(面试被问过几次),这个后期会出一篇,会尽量讲详细一点。
不要打瞌睡,这是面试官想看你的内功。
keep-alive本身渲染
刚刚说了,keep-alive自身组件不会被渲染到页面上,那是怎么做到的呢?其实就是通过判断组件实例上的abstract
的属性值,如果是true的话,就跳过该实例,该实例也不会出现在父级链上。
// 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
// ...
}
包裹组件渲染
咱们再来说说被keep-alive包裹着的组件是如何使用缓存的吧。
刚刚说了VNode -> 真实DOM是发生在patch
的阶段,而其实这也是要细分的:VNode -> 实例化 -> _update -> 真实DOM,而组件使用缓存的判断就发生在实例化这个阶段,而这个阶段调用的是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 */)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm) // 将缓存的DOM(vnode.elm)插入父元素中
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
-
在第一次加载被包裹组件时,因为keep-alive的
render
先于包裹组件加载之前执行,所以此时vnode.componentInstance
的值是undefined,而keepAlive是true,则代码走到i(vnode, false /*hydrating */)就不往下走了 -
再次访问包裹组件时,
vnode.componentInstance
的值就是已经缓存的组件实例,那么会执行insert
(parentElm, vnode.elm, refElm)逻辑,这样就直接把上一次的DOM插入到了父元素中。
面试官摸了摸胡子,拍了拍我,明天来报道。
LRU缓存函数
后记
我们再来总结一下回答:
🙋🏻♂️我个人主要从四个方面回答
-
缓存用keep-alive,它的作用与用法
- keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
-
使用细节,例如缓存指定/排除、结合router和transition
- 结合属性include和exclude可以明确指定缓存哪些组件或排除缓存指定组件。vue3中结合vue-router时变化较大,之前是keep-alive包裹router-view,现在相反。
-
组件缓存后更新可以利用activated或者beforeRouteEnter
-
原理阐述
- 它内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode,如果该组件在map中存在就直接返回它。由于component的is属性是个响应式数据,因此只要它变化,keep-alive的render函数就会重新执行。
其实按照这个思路回答不算难,但可能面试官想让你回答背后的原理。(可反复多看几次)
回答面试题的时候,尽量形成一个结构化的回答,大体,再细化,由浅而深。
我是Dignity_呱,来交个朋友呀,有朋自远方来,不亦乐乎呀!深夜末班车
👍 如果对您有帮助,您的点赞是我前进的润滑剂。