• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

LRU算法在keepalive组件源码里是怎么用到的

武飞扬头像
超人不会飛
帮助15

什么是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>作用流程:

  1. 当一个<keep-alive>包裹组件第一次渲染时,该组件会被存起来,并且例会被保留在内存中。

  2. 当这个组件被切换出去例如,通过v-if或路由导离开了该组件),它并不会被销毁,而是早已经被放到一个名为cache的缓存对象。

  3. 如果之后再次切换回这个组,它会从缓存中取,并重新插入到DOM中,而不是重新创建一个新的实例。

  4. 在组件被缓存期间,它的生命周期钩子函数不会被调用。但是,activateddeactivated这两个特殊的生命周期钩子函数会在组件被激活和停用时被调用。

  5. 如果需要对缓存的件进行操作,可以使用<keep-alive>的特殊属性includeexclude来指定哪些组件需要被缓存或排除缓存之外。

总结来说<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这个钩子中对includeexclude参数进行监听,然后实时地更新(删除)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,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该keythis.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 为 undefinedvnode.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.componentInstancekeepAlive同时为truly值时,不再进入$mount过程,那mounted之前的所有钩子函数(beforeCreatecreatedmounted)都不再执行。

当一个组件实例从 DOM 上移除但因为被 <KeepAlive> 缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活

patch的阶段,那么可以通过 onActivated() 和 onDeactivated() 注册相应的两个状态的生命周期钩子:

<script setup>
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  // 调用时机为首次挂载
  // 以及每次从缓存中被重新插入时
})

onDeactivated(() => {
  // 在从 DOM 上移除、进入缓存
  // 以及组件卸载时调用
})
</script>

请注意:

  • onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用。
  • 这两个钩子不仅适用于 <KeepAlive> 缓存的根组件,也适用于缓存树中的后代组件。

参考:

juejin.cn/post/684490…

cn.vuejs.org/guide/built…

ustbhuangyi.github.io/vue-analysi…

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfffike
系列文章
更多 icon
同类精品
更多 icon
继续加载