靓仔,说一下keep-alive缓存组件后怎么更新及原理?

10,581 阅读12分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」 

前言

  • 常网IT戳我呀!
  • 常网IT源码上线啦!
  • 本篇录入吊打面试官专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。
  • 这段时间面了很多家公司,被问到的题我感觉不重复不止100道,将会挑选觉得常见且有意义的题目进行分析及回答。
  • 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。
  • 请问Vue中的keep-alive怎么缓存组件的,缓存后又是怎么更新的,背后的原理你造吗?

这两天屁股感觉有点微疼,这不周六就去看了一下,说是痔疮,没想到也中招了。
说手术只会让你长的这块地方不会长,但会从别处长起,不会消失,只会转移。
都说十人九人会,我是没机会做那其中的一人了。
避免久坐,多摸鱼才能有好转。

未命名图片.jpg

若面试着急用,最后面有回答示例~

一、问题剖析

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>

第二

结合属性includeexclude可以明确指定缓存哪些组件或排除缓存指定组件。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的原理是什么?

面试官说:不把你难倒,我就对不起我刁难官的称号。

4.jpg

场景

回答原理之前,我们先来看看一个场景。

可能大家在平时的开发中会经常遇到这样的场景:有一个可以进行筛选的列表页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阶段做了什么?):包括咱这里提到的渲染。

image.png

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插入到了父元素中。

面试官摸了摸胡子,拍了拍我,明天来报道。

3.jpeg

LRU缓存函数

代码传送👉

image.png

后记

我们再来总结一下回答:

🙋🏻‍♂️我个人主要从四个方面回答

  • 缓存用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_呱,来交个朋友呀,有朋自远方来,不亦乐乎呀!深夜末班车

👍 如果对您有帮助,您的点赞是我前进的润滑剂。

以往推荐

面试官问我new Vue阶段做了什么?

面试官问我watch和computed的区别以及选择?

前端仔,快把dist部署到Nginx上

多图详解,一次性啃懂原型链(上万字)

Vue-Cli3搭建组件库

Vue实现动态路由(和面试官吹项目亮点)

项目中你不知道的Axios骚操作(手写核心原理、兼容性)

VuePress搭建项目组件文档

原文链接

juejin.cn/post/716567…