Принцип реализации LRU-кэш-поддержки

внешний интерфейс JavaScript Vue.js
Принцип реализации LRU-кэш-поддержки

泠然.png

Это 124-я оригинальная статья без воды.Если вы хотите получить больше оригинальных статей, выполните поиск в официальном аккаунте и подпишитесь на нас~ Эта статья была впервые опубликована в блоге Zhengcaiyun:Принцип реализации LRU-кэш-поддержки

предисловие

Я полагаю, что у большинства студентов более или менее будет сцена, в которой состояние компонента нужно сохранять, а не перерисовывать при разработке повседневных потребностей.Студенты, знакомые с vue, обязательно придумают это.keep-alive Это встроенный компонент.

так что жеkeep-aliveШерстяная ткань?

keep-aliveявляется встроенным компонентом Vue.js. Он может хранить экземпляры неактивных компонентов в памяти, а не уничтожать их напрямую. Это абстрактный компонент, который не будет отображаться в реальном DOM и не появится в цепочке родительских компонентов. Проще говоря, keep-alive используется для сохранения состояния рендеринга компонентов, предотвращения повторного создания и рендеринга компонентов и эффективного повышения производительности системы.keep-aliveизmaxАтрибуты для ограничения количества кэшируемых экземпляров компонента, когда это число достигает верхнего предела, перед созданием нового экземпляра пример, в котором к кэшированному компоненту не было доступа, и используемый здесь механизм кэшированияАлгоритм LRU

Алгоритм устранения кеша LRU

LRU (наименее недавно использовавшийся) отсеивает данные на основе своей истории, сосредотачиваясь наЗащитите недавно использованные/использованные данные и удалите данные, к которым не обращались в течение длительного времени на этом этапе.

Основная идея LRU заключается в том, что если доступ к данным был осуществлен недавно, то вероятность обращения к ним в будущем также выше.

fifo对比lru原理
  1. Новые данные вставляются в конец связанного списка;
  2. Всякий раз, когда происходит попадание в кеш (то есть доступ к кэшированным данным), данные перемещаются в конец связанного списка.
  3. Когда связанный список заполнен, данные в начале связанного списка отбрасываются.

Структура данных, реализующая LRU

Обычно используется классический LRU.hashMap + 双向链表. Учитывая, что может быть необходимо часто удалять элемент и указывать предыдущий узел этого элемента на следующий узел, наиболее целесообразно использовать двойную ссылку. И он хранится в хронологическом порядке самых последних использованных узлов. Если узел посещается, у нас есть основания полагать, что его вероятность быть посещенным в следующий период больше, чем у других узлов.

map.keys()

Но поскольку он использовался в jsMapТеперь вы будете использовать готовый итератор для получения значения ключа следующего узла (keys().next( ))

// ./LRU.ts
export class LRUCache {
  capacity: number; // 容量
  cache: Map<number, number | null>; // 缓存
  constructor(capacity: number) {
    this.capacity = capacity;
    this.cache = new Map();
  }
  get(key: number): number {
    if (this.cache.has(key)) {
      let temp = this.cache.get(key) as number;
      //访问到的 key 若在缓存中,将其提前
      this.cache.delete(key);
      this.cache.set(key, temp);
      return temp;
    }
    return -1;
  }
  put(key: number, value: number): void {
    if (this.cache.has(key)) {
      this.cache.delete(key);
      //存在则删除,if 结束再提前
    } else if (this.cache.size >= this.capacity) {
      // 超过缓存长度,淘汰最近没使用的
      this.cache.delete(this.cache.keys().next().value);
      console.log(`refresh: key:${key} , value:${value}`)
    }
    this.cache.set(key, value);
  }
  toString(){
    console.log('capacity',this.capacity)
    console.table(this.cache)
  }
}
// ./index.ts
import {LRUCache} from './lru'
const list = new LRUCache(4)
list.put(2,2)   // 入 2,剩余容量3
list.put(3,3)   // 入 3,剩余容量2
list.put(4,4)   // 入 4,剩余容量1
list.put(5,5)   // 入 5,已满    从头至尾         2-3-4-5
list.put(4,4)   // 入4,已存在 ——> 置队尾         2-3-5-4
list.put(1,1)   // 入1,不存在 ——> 删除队首 插入1  3-5-4-1
list.get(3)     // 获取3,刷新3——> 置队尾         5-4-1-3
list.toString()
// ./index.ts
import {LRUCache} from './lru'
const list = new LRUCache(4)

list.put(2,2)   // 入 2,剩余容量3
list.put(3,3)   // 入 3,剩余容量2
list.put(4,4)   // 入 4,剩余容量1
list.put(5,5)   // 入 5,已满    从头至尾  				2-3-4-5
list.put(4,4)   // 入4,已存在 ——> 置队尾  				2-3-5-4
list.put(1,1)   // 入1,不存在 ——> 删除队首 插入1  3-5-4-1
list.get(3)     // 获取3,刷新3——> 置队尾  				5-4-1-3
list.toString()

Результат выглядит следующим образом:lru打印结果.jpg

Keep-Alive в vue

принцип

  1. Используйте механизм кэширования LRU для кэширования, max ограничивает максимальную емкость кэш-таблицы.
  2. В соответствии с набором include/exclude (если есть) выполняется условное сопоставление, чтобы решить, нужно ли кэшировать. Не соответствует, напрямую возвращает экземпляр компонента
  3. Создайте ключ кэша на основе идентификатора компонента и тега и проверьте, был ли экземпляр компонента кэширован в объекте кэша. Если он существует, напрямую извлеките кэшированное значение и обновите позицию ключа в this.keys (обновление позиции ключа является ключом к реализации стратегии замены LRU).
  4. Получите название узла или узла в соответствии с заклинанием Название текущей информации компонента CID
  5. Получите первый объект дочернего компонента, обернутый keep-alive, и имя его компонента

Анализ исходного кода

Инициализировать компонент keepAlive
const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,
  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;
    // 当 props 上的 include 或者 exclude 变化时移除缓存
    watch(
      () => [props.include, props.exclude],
      ([include, exclude]) => {
      include && pruneCache((name) => matches(include, name));
      exclude && pruneCache((name) => !matches(exclude, name));
      },
      { flush: "post", deep: true }
    );
    // 缓存组件的子树 subTree
    let pendingCacheKey: CacheKey | null = null;
    const cacheSubtree = () => {
      // fix #1621, the pendingCacheKey could be 0
      if (pendingCacheKey != null) {
        cache.set(pendingCacheKey, getInnerChild(instance.subTree));
      }
    };
    // KeepAlive 组件的设计,本质上就是空间换时间。
    // 在 KeepAlive 组件内部,
    // 当组件渲染挂载和更新前都会缓存组件的渲染子树 subTree
    onMounted(cacheSubtree);
    onUpdated(cacheSubtree);
    onBeforeUnmount(() => {
    // 卸载缓存表里的所有组件和其中的子树...
    }
    return ()=>{
      // 返回 keepAlive 实例
    }
  }
}

return ()=>{
  // 省略部分代码,以下是缓存逻辑
  pendingCacheKey = null
  const children = slots.default()
  let vnode = children[0]
  const comp = vnode.type as Component
  const name = getName(comp)
  const { include, exclude, max } = props
  // key 值是 KeepAlive 子节点创建时添加的,作为缓存节点的唯一标识
  const key = vnode.key == null ? comp : vnode.key
  // 通过 key 值获取缓存节点
  const cachedVNode = cache.get(key)
  if (cachedVNode) {
    // 缓存存在,则使用缓存装载数据
    vnode.el = cachedVNode.el
    vnode.component = cachedVNode.component
    if (vnode.transition) {
      // 递归更新子树上的 transition hooks
      setTransitionHooks(vnode, vnode.transition!)
    }
      // 阻止 vNode 节点作为新节点被挂载
      vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
      // 刷新key的优先级
      keys.delete(key)
      keys.add(key)
  } else {
      keys.add(key)
      // 属性配置 max 值,删除最久不用的 key ,这很符合 LRU 的思想
      if (max && keys.size > parseInt(max as string, 10)) {
        pruneCacheEntry(keys.values().next().value)
      }
    }
    // 避免 vNode 被卸载
    vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
    current = vnode
    return vnode;
}
Переместить компоненты из кэш-таблицы
// 遍历缓存表
function pruneCache(filter?: (name: string) => boolean) {
  cache.forEach((vnode, key) => {
    const name = getComponentName(vnode.type as ConcreteComponent);
    if (name && (!filter || !filter(name))) {
      // !filter(name) 即 name 在 includes 或不在 excludes 中
      pruneCacheEntry(key);
    }
  });
}
// 依据 key 值从缓存表中移除对应组件
function pruneCacheEntry(key: CacheKey) {
  const cached = cache.get(key) as VNode;
  if (!current || cached.type !== current.type) {
    /* 当前没有处在 activated 状态的组件
     * 或者当前处在 activated 组件不是要删除的 key 时
     * 卸载这个组件
    */
    unmount(cached); // unmount方法里同样包含了 resetShapeFlag
  } else if (current) {
    // 当前组件在未来应该不再被 keepAlive 缓存
    // 虽然仍在 keepAlive 的容量中但是需要刷新当前组件的优先级
    resetShapeFlag(current);
    // resetShapeFlag 
  }
  cache.delete(key);
  keys.delete(key);
}
function resetShapeFlag(vnode: VNode) {
  let shapeFlag = vnode.shapeFlag; // shapeFlag 是 VNode 的标识
   // ... 清除组件的 shapeFlag
}

случай поддержания активности

В этом разделе будут использоваться новые функции vue 3.x для имитацииkeep-aliveконкретные сценарии применения

В index.vue мы представили CountUp, таймер и ColorRandom с тремя компонентами состояния. вместимостью 2<keep-alive>оборачивает динамический компонент в

// index.vue
<script setup>
import { ref } from "vue"
import CountUp from '../components/CountUp.vue'
import ColorRandom from '../components/ColorRandom.vue'
import Timer from '../components/Timer.vue'
const tabs = ref([    // 组件列表
  {
    title: "ColorPicker",
    comp: ColorRandom,
  },
  {
    title: "timer1",
    comp: Timer,
  },
  {
    title: "timer2",
    comp: Timer,
  },
  {
    title: "CountUp",
    comp: CountUp,
  },
])
const currentTab = ref(tabs.value[0]) // tab 默认展示第一个组件
const tabSwitch = (tab) => {
  currentTab.value = tab
}
</script>
<template>
  <div id="main-page">keep-alive demo below</div>
  <div class="tab-group">
    <button
    v-for="tab in tabs"
    :key="tab"
    :class="['tab-button', { active: currentTab === tab }]"
    @click="tabSwitch(tab)"
  >
    {{ tab.title }}
  </button>
  </div>
  <keep-alive max="2">
  <!-- 动态组件渲染 tab 当前的组件 -->  
    <component
      v-if="currentTab"
      :is="currentTab.comp"
      :key="currentTab.title"
      :name="currentTab.title"
    />
  </keep-alive>
</template>

состояние кэша

Процесс кэширования выглядит следующим образом:

缓存流程图

можно увидеть завернутым вkeep-aliveДинамический компонент кэша кэширует состояние предыдущего компонента.

Наблюдая за изменениями узлов в vue devtools, вы можете видеть, что keepAlive содержитColorRandomа такжеTimerДва компонента, текущий отображаемый компонент будет в активированном состоянии, а другие кэшированные компоненты будут в активированном состоянии.inactivatedположение дел

Если мы аннотируем обаkeep-aliveВы обнаружите, что независимо от того, как вы переключаете компоненты, они будут только повторно отображаться и не сохранят предыдущее состояние.

keepAlive-cache.gif

удалить компоненты

Процесс удаления выглядит следующим образом:

移除流程图

Чтобы проверить, можно ли успешно удалить компонент при переключении вкладок, в каждом компонентеonUnmountedдобавлен журнал

onUnmounted(()=>{
  console.log(`${props.name} 组件被卸载`)
}) 
  • Когда длина кэшированных данных меньше или равна max , переключение компонентов не приведет к удалению других компонентов, как показано выше в vue devtools, оно только вызовет запуск компонента.activatedа такжеdeactivatedдва жизненных цикла
  • Если в это время длина кэшированных данных больше max, компонент с более низким приоритетом будет удален из списка кэшей, и соответственно будет виден компонент, который будет удален первым.umountedТриггеры жизненного цикла.
keepAlive-unmount.gif

оптимизация производительности

После использования KeepAlive vnode и DOM компонента, обернутого KeepAlive, будут кэшироваться после первого рендеринга, а затем при следующем повторном рендеринге компонента соответствующие vnode и DOM будут получены непосредственно из кеша, а затем выполняется рендеринг. не нужно проходить ряд процессов, таких как инициализация компонента, рендеринг и исправление, что сокращает время выполнения скрипта и повышает производительность.

Суммировать

Vue внутренне абстрагирует узлы DOM в узлы VNode, а кеш компонентов поддержки активности также основан на узлах VNode вместо непосредственного хранения структуры DOM. Он кэширует компоненты, которые соответствуют условиям (включают и исключают) в объекте кеша, а затем берет узел vnode из объекта кеша и визуализирует его, когда требуется повторный рендеринг.

Конкретный процесс кэширования выглядит следующим образом:

  1. Объявите упорядоченный набор ключей в качестве контейнера кеша и сохраните уникальное значение ключа компонента.

  2. В ключах контейнера кеша чем выше значение ключа, тем меньше доступа и приоритета, которые нужно исключить.

  3. Когда функция рендеринга выполняется, если кеш попал, текущая нажатая клавиша удаляется из клавиш, а значение ключа добавляется в конец клавиш, чтобы обновить приоритет ключа.

  4. Когда кеш не попадает, ключи добавляют значение ключа данных кеша.Если длина данных кеша больше максимального значения, самые старые данные будут удалены.

  5. При запуске жизненного цикла beforeMount/update кэшировать данные поддерева текущего активированного компонента


Ссылаться на

Рекомендуемое чтение

работы с открытым исходным кодом

  • Zhengcaiyun интерфейсный таблоид

адрес с открытым исходным кодомwww.zoo.team/openweekly/(На главной странице официального сайта таблоида есть группа обмена WeChat)

  • Плагин выбора товара

адрес с открытым исходным кодомGitHub.com/Chinese Patent Medicine-Inc/Reservoir…

Карьера

ZooTeam, молодая, увлеченная и творческая команда, связанная с отделом исследований и разработок продукции Zhengcaiyun, базируется в живописном Ханчжоу. В настоящее время в команде более 60 фронтенд-партнеров, средний возраст которых составляет 27 лет, и почти 40% из них — инженеры полного стека, настоящая молодежная штурмовая группа. В состав членов входят «ветераны» солдат из Ali и NetEase, а также первокурсники из Чжэцзянского университета, Университета науки и технологий Китая, Университета Хандянь и других школ. В дополнение к ежедневным деловым связям, команда также проводит технические исследования и фактические боевые действия в области системы материалов, инженерной платформы, строительной платформы, производительности, облачных приложений, анализа и визуализации данных, а также продвигает и внедряет ряд внутренних технологий. Откройте для себя новые горизонты передовых технологических систем.

Если вы хотите измениться, вас забрасывают вещами, и вы надеетесь начать их бросать; если вы хотите измениться, вам сказали, что вам нужно больше идей, но вы не можете сломать игру; если вы хотите изменить , у вас есть возможность добиться этого результата, но вы не нужны; если вы хотите изменить то, чего хотите достичь, вам нужна команда для поддержки, но вам некуда вести людей; если вы хотите изменить установившийся ритм, это будет "5 лет рабочего времени и 3 года стажа работы"; если вы хотите изменить исходный Понимание хорошее, но всегда есть размытие того слоя оконной бумаги.. , Если вы верите в силу веры, верьте, что обычные люди могут достичь необыкновенных вещей, и верьте, что они могут встретить лучшего себя. Если вы хотите участвовать в процессе становления бизнеса и лично способствовать росту фронтенд-команды с глубоким пониманием бизнеса, надежной технической системой, технологиями, создающими ценность, и побочным влиянием, я думаю, что мы должны говорить. В любое время, ожидая, пока вы что-нибудь напишете, отправьте это наZooTeam@cai-inc.com