Решения Vue для кэширования на уровне страниц

Vue.js

feb-alive

Причина использования

  • Разработчикам не нужно прописывать логику инициализации данных в разных хуках перед RouteUpdate или активировать из-за различий в динамической маршрутизации или общей маршрутизации.
  • Разработчикам не нужно вручную кэшировать состояние страницы, например кэшировать данные текущей страницы через localStorage или sessionStorage.
  • Feb-alive поможет вам обрабатывать хранение и восстановление метаинформации маршрута

Зачем разрабатывать feb-laive?

Когда мы разрабатываем проекты с помощью Vue, будут ли следующие требования к сценарию?

  1. /aперенаправить на/b
  2. вернуться к/aкогда вы хотите восстановить страницу из кеша
  3. снова прыгнуть в/b, есть два случая
    1. Случай 1: Перейти по ссылкам или нажать, вы хотите воссоздать/bстраница вместо чтения из кеша
    2. Случай 2: Если вы нажмете собственную кнопку браузера вперед, страница все равно будет считана из кеша.

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

Попробуйте реализовать кэширование страниц с помощью Keep-alive

<keep-alive>
  <router-view></router-view>
</keep-alive>

так легко, но идеально идеально, реальность очень жестока

Существует проблема

-/aПрыгать/b, Прыгать в/a/a

  • Точно так же переход динамической маршрутизации/page/1->/page/2Поскольку две страницы ссылаются на один и тот же компонент, страница не изменится при переходе, потому что ключ кеша поддержки активности генерируется в соответствии с компонентом (конечно, Vue предоставляет нам хук beforeRouteUpdate для обновления данных).
  • Резюме: кэш проверки активности == уровень компонента ==, а не == уровень страницы ==.

Дайте сценарий применения

Например, просмотрите страницу статьи и посетите 3 статьи в свою очередь

  1. /artical/1
  2. /artical/2
  3. /artical/3

когда я из/artical/3вернуться к/artical/2В это время из-за кеша компонента страница по-прежнему содержит содержимое статьи 3, поэтому данные страницы 2 должны быть повторно загружены через beforeRouteUpdate. (Обратите внимание, что возвращение сюда не вызовет активированный хук компонента, потому что оба маршрута отображают один и тот же компонент, поэтому экземпляр будет использоваться повторно, а reactivateComponent не будет выполняться.)

если хочешь от/artical/3вернуться к/artical/2, при попытке восстановить предыдущую/artical/2некоторые государства в/artical/2Все данные о состоянии сохраняются и восстанавливаются.

Подводя итог: все еще существует разрыв между кешем на уровне компонентов, реализованным keep-alive, и кешем, который мы себе представляли, и keep-alive не может удовлетворить наши потребности.

==В ответ на эти проблемы родился плагин feb-alive==

export default {
  name: 'keep-alive',
  abstract: true,

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },

  created () {
    this.cache = Object.create(null)
    this.keys = []
  },

  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render () {
    // 获取默认插槽
    const slot = this.$slots.default
    // 获取第一个组件,也就和官方说明的一样,keep-alive要求同时只有一个子元素被渲染,如果你在其中有 v-for 则不会工作。
    const vnode: VNode = getFirstComponentChild(slot)
    // 判断是否存在组件选项,也就是说只对组件有效,对于普通的元素则直接返回对应的vnode
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // 检测include和exclude
      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
      // 如果指定了子组件的key则使用,否则通过cid+tag生成一个key
      const key: ?string = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      // 判断是否存在缓存
      if (cache[key]) {
        // 直接复用组件实例,并更新key的位置
        vnode.componentInstance = cache[key].componentInstance
        remove(keys, key)
        keys.push(key)
      } else {
        // 此处存储的vnode还没有实例,在之后的流程中通过在createComponent中会生成实例
        cache[key] = vnode
        keys.push(key)
        // 当缓存数量大于阈值时,删除最早的key
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }
      // 设置keepAlive属性,createComponent中会判断是否已经生成组件实例,如果是且keepAlive为true则会触发actived钩子。
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}

Alive-Alive - это абстрактный компонент, а в экземпляре компонента поддерживается кеш, который является следующей частью кода

created () {
  // 存储组件缓存
  this.cache = Object.create(null)
  this.keys = []
}

let Foo = {
    template: '<div class="foo">foo component</div>',
    name: 'Foo'
}
let Bar = {
    template: '<div class="bar">bar component</div>',
    name: 'Bar'
}
let gvm = new Vue({
    el: '#app',
    template: `
        <div id="#app">
            <keep-alive>
                <component :is="renderCom"></component>
            </keep-alive>
            <button @click="change">切换组件</button>
        </div>
    `,
    components: {
        Foo,
        Bar
    },
    data: {
        renderCom: 'Foo'
    },
    methods: {
        change () {
            this.renderCom = this.renderCom === 'Foo' ? 'Bar': 'Foo'
        }
    }
})

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

function anonymous(
) {
  with(this){return _c('div',{attrs:{"id":"#app"}},[_c('keep-alive',[_c(renderCom,{tag:"component"})],1),_c('button',{on:{"click":change}})],1)}
}

Доступна онлайн-компиляция:Talent.v ue js.org/v2/expensive/hot…

Согласно приведенной выше функции рендеринга, можно узнать, что процесс генерации vnode глубоко рекурсивен: сначала создается vnode дочернего элемента, а затем создается vnode родительского элемента. Таким образом, при первом рендеринге, когда генерируется vnode компонента поддержки активности, vnode компонента Foo был сгенерирован и передан в качестве параметра конструктора vnode компонента поддержки активности (_c).

_c('keep-alive',[_c(renderCom,{tag:"component"})

Vnode сгенерированного компонента для хранения живой является следующим образом

{
    tag: 'vue-component-2-keep-alive',
    ...
    children: undefined,
    componentInstance: undefined,
    componentOptions: {
        Ctor: f VueComponent(options),
        children: [Vnode],
        listeners: undefined,
        propsData: {},
        tag: 'keep-alive'
    },
    context: Vue {...}, // 调用 $createElement/_c的组件实例, 此处是根组件实例对象
    data: {
       hook: {
           init: f,
           prepatch: f,
           insert: f,
           destroy: f
       } 
    }
}

Здесь следует отметить, что Vnode компонента не имеет детей, но оригинальные дети используются в качестве дети свойства компонентов компонентов Vnode. Компоненты будут использоваться, когда компонент создается, а компоненты. Назначено VM. $ Слоты, часть исходного кода выглядит следующим образом

// createComponent函数
function createComponent (Ctor, data, context, children, tag) {
    // 此处省略部分代码
    ...
    var vnode = new VNode(
        ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
        data, undefined, undefined, undefined, context,
        { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
        asyncFactory
    );
    return vnode
}

Vue, наконец, выполнит рендеринг с помощью функции patch, преобразует vnode в настоящий дом и будет рендерить компоненты с помощью createComponent.

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    var i = vnode.data;
    if (isDef(i)) {
      var 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
      }
    }
  }

Следующие два шага

  1. Отрисовка самого компонента keep-alive
  2. keep-alive оборачивает рендеринг компонентов, в данном случае компонентов Foo и Bar.

Поговорим о рендеринге самого keep-alive компонента в этом примере

  1. создание экземпляра корневого компонента
  2. корневой компонент $mount
  3. Корневой компонент вызывает mountComponent
  4. Корневой компонент генерирует renderWatcher
  5. Корневой компонент вызывает updateComponent
  6. Корневой компонент вызывает vm.render() для создания vnode корневого компонента.
  7. Корневой компонент вызывает vm.update(vnode)
  8. Корневой компонент вызывает vm.patch(oldVnode, vnode)
  9. Координационный компонент вызывает Creteeeeeeeeever (Vnode)
  10. При рендеринге дочерних элементов, если они типа компонента, vnode вызывается createComponent (vnode), и именно в этом процессе были созданы подсборки и инстанцировано монтирование ($ mount)

createElm(keepAliveVnode)Примеры процесса и будут поддерживать компоненты монтирования, а в примере процесса инкапсулированный в подсборке узел поддержки активности будет назначен экземпляру компонента $ слота атрибута поддержки активности, поэтому сохраняйте при вызове функции. Например, слот can.$ для получения этой сборки пакета vnode, в демо-версии, сборка Foo vnode, специальная функция анализа рендеринга компонента keep-alive

render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      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
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        remove(keys, key)
        keys.push(key)
      } else {
        cache[key] = vnode
        keys.push(key)
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }

Как было проанализировано выше, при выполненииcreateElm(keepAliveVnode)В ходе этого процесса будет выполняться создание и монтирование компонента поддержки активности.($mount), и в процессе монтирования будет выполняться функция рендеринга keep-alive. Как было проанализировано ранее, в функции рендеринга vnode подкомпонента может быть получен через this.$slot. Из приведенного выше исходного кода вы можете узнать что keep-alive обрабатывает только первый подкомпонент слота по умолчанию.Подразумевается, что если несколько компонентов заключены в keep-alive, остальные компоненты будут проигнорированы, например:

<keep-alive>
    <Foo />
    <Bar />
</keep-alive>
// 只会渲染Foo组件

Продолжить анализ. После получения vnode компонента Foo мы оцениваем componentOptions. Поскольку наш Foo является компонентом, здесь есть componentOptions. В логике if include означает, что будут кэшироваться только совпадающие компоненты, а exclude указывает, что любые совпадающие компоненты не будет кэшироваться, а в демо не заданы соответствующие правила, которые здесь будут проигнорированы.

const { cache, keys } = this

cache, ключи генерируются в хуке create компонента keep-alive и используются для хранения экземпляра компонента, кэшированного keep-alive, и ключа соответствующего vnode

created () {
    this.cache = Object.create(null)
    this.keys = []
}

продолжить ниже

const key: ?string = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
if (cache[key]) {
    vnode.componentInstance = cache[key].componentInstance
    remove(keys, key)
    keys.push(key)
} else {
    cache[key] = vnode
    keys.push(key)
    if (this.max && keys.length > parseInt(this.max)) {
      pruneCacheEntry(cache, keys[0], keys, this._vnode)
    }
}

Во-первых, выньте ключ vnode, если vnode.key существует, используйте vnode.key, если он не существует, используйте vnode.keycomponentOptions.Ctor.cid + (componentOptions.tag ? ::${componentOptions.tag} : '')В качестве ключа для хранения экземпляра компонента можно знать, что если мы не укажем ключ компонента, тот же компонент будет сопоставлен с тем же кешем, поэтому при описании подчеркивается, что это уровень компонента. keep-alive в начале схема кэширования.

Тогда при первом рендеринге кеш и ключи будут пусты, и здесь будет следовать логика else.

cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
  pruneCacheEntry(cache, keys[0], keys, this._vnode)
}

Сохраните vnode компонента Foo с ключом в качестве ключа кеша(注意此时vnode上面还没有componentInstance), здесь используется принцип хранения объектов, и тогда при инстанцировании компонента Foo его экземпляр будет присвоен vnode.componentInstance, тогда vnode.componentInstance можно будет получить при следующем рендеринге keep-alive компонента.

Таким образом, первый рендеринг происходит только в кэше поддержки активности, в котором хранится vnode, обертывающий компонент Foo.

Рендеринг для обернутых компонентов

В этом примере из-за изменения свойства renderCom сработает renderWatcher корневого компонента, а затем будет выполнен patch(oldVnode, vnode). При сравнении дочерних vnode старые и новые vnode keep-alive будут оценены как sameVnode, а затем войдет в логику patchVnode.

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    if (oldVnode === vnode) {
      return
    }
    // 此处省略代码
    ...
    var i;
    var data = vnode.data;
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode);
    }
    // 此处省略代码
    ...
}

prepatch: function prepatch (oldVnode, vnode) {
    var options = vnode.componentOptions;
    var child = vnode.componentInstance = oldVnode.componentInstance;
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    );
}

Можно увидеть, что примеры живой во время сборки корневого элемента будут повторно отображать мультиплексирование, что также гарантирует, что экземпляр компонента до того, как все еще существует в кеше памяти After Alive

var child = vnode.componentInstance = oldVnode.componentInstance;

последующийupdateChildComponentЭта функция очень критична, эта функция служит ключевой задачей переключения компонента Foo на компонент Bar. Мы знаем, что, поскольку компонент keep-alive используется здесь повторно, он больше не сработает.initRender, поэтому vm.$slot больше не будет обновляться. так вupdateChildComponentФункция берет на себя ответственность за обновление слота

function updateChildComponent (
  vm,
  propsData,
  listeners,
  parentVnode,
  renderChildren
) {
  if (process.env.NODE_ENV !== 'production') {
    isUpdatingChildComponent = true;
  }

  // determine whether component has slot children
  // we need to do this before overwriting $options._renderChildren
  var hasChildren = !!(
    renderChildren ||               // has new static slots
    vm.$options._renderChildren ||  // has old static slots
    parentVnode.data.scopedSlots || // has new scoped slots
    vm.$scopedSlots !== emptyObject // has old scoped slots
  );

  // ...

  // resolve slots + force update if has children
  if (hasChildren) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context);
    vm.$forceUpdate();
  }

  if (process.env.NODE_ENV !== 'production') {
    isUpdatingChildComponent = false;
  }
}

Функция updateChildComponent в основном обновляет некоторые свойства текущего экземпляра компонента, включая props, listeners и slots. Давайте сосредоточимся на обновлении слота.Здесь последний vnode обернутого компонента получается через resolveSlots, который является компонентом Bar в демо, а затем компонент keep-alive принудительно перерисовывается через vm.$forceUpdate. (Советы: когда у нашего компонента есть слот, экземпляр компонента $fourceUpdate будет запущен при повторном рендеринге родительского компонента компонента. Здесь будет потеря производительности, потому что независимо от того, влияет ли изменение данных на слот , форс сработает. Обновите, судя по интродукции на vueConf, эта проблема будет оптимизирована в 3.0), например

// Home.vue
<template>
    <Artical>
        <Foo />
    </Artical>
</tempalte>

В этом примере, когда компонент Home обновляется, он запускает принудительное обновление компонента Artical, и это обновление является избыточным.

Продолжить после обновления экземпляра keep-aliveslotsПосле этого непосредственно срабатываетkeepaliveэкземпляр компонентаПосле слотов непосредственно запускается экземпляр компонента поддержки активности.

render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    // ...
}

Когда компонент переключается из бара в Foo снова

Логика для компонента keep-alive остается такой же, как описано выше.

  1. выполнить препатч
  2. Повторное использование экземпляров компонента проверки активности
  3. Выполните updateChildComponent для обновления $slots
  4. вызвать vm.$forceUpdate
  5. Запуск функции рендеринга компонента поддержки активности

Снова введите функцию рендеринга. На этот раз cache[key] будет соответствовать vnode, кэшированному при первом рендеринге компонента Foo. Посмотрите на эту часть логики.

const key: ?string = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
if (cache[key]) {
    vnode.componentInstance = cache[key].componentInstance
    remove(keys, key)
    keys.push(key)
} else {
    cache[key] = vnode
    keys.push(key)
    if (this.max && keys.length > parseInt(this.max)) {
      pruneCacheEntry(cache, keys[0], keys, this._vnode)
    }
}

Поскольку компонент, обернутый keep-alive, является компонентом Foo, согласно правилам, ключ, сгенерированный в это время, совпадает с ключом, сгенерированным при первом рендеринге компонента Foo, поэтому в этот раз входит функция рендеринга keep-alive. первая if branch , то есть совпадает с cache[key], присваивает закэшированный componentInstance текущему vnode, а затем обновляет ключи (когда есть max, можно гарантировать удаление более старого кэша).

Многие студенты могут спросить, каков эффект установки здесь vnode.componentInstance. Это включает в себя часть исходного кода vue.

Поскольку он переключается с компонента Bar на компонент Foo, при сравнении патча он не будет оцениваться как тот же Vnode, поэтому он, естественно, переходит к createElm.Поскольку Foo является компонентом Vue, он войдет в createComponent, поэтому в end Введите следующий фрагмент функции

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    var i = vnode.data;
    if (isDef(i)) {
      var 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
      }
    }
  }

Согласно анализу исходного кода keep-alive выше, здесь isReactivated имеет значение true, и тогда он войдет в функцию инициализации жизненного цикла, которая зависает при генерации vnode

var componentVNodeHooks = {
  init: function init (vnode, hydrating) {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      var mountedNode = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    } else {
      var child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      );
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
  },
  prepatch: function prepatch (oldVnode, vnode) {
    var options = vnode.componentOptions;
    var child = vnode.componentInstance = oldVnode.componentInstance;
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    );
  },
  ...
}

В это время, поскольку экземпляр уже существует, и keepalive верно, первое, если будет предпринято логику, предводит, будет выполнено, свойства компонента и некоторые слушатели будут обновлены, если есть слот, слот также будет обновлен, а также $ ForceUpdate будет выполнена, это было проанализировано ранее, поэтому я не буду повторять его.

Продолжайте создавать компоненты, initComponent и вставка будут выполняться внутри функции.

if (isDef(vnode.componentInstance)) {
    // 将实例上的dom赋值给vnode
    initComponent(vnode, insertedVnodeQueue);
    // 插入dom
    insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
    reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}

До сих пор, когда компонент снова переключается с Bar на Foo, и экземпляр, и дом используются повторно, что обеспечивает высокий эффект опыта! И feb-alive, который мы реализуем позже, основан на keep-alive.

Решение Vue для кэширования на уровне страниц в феврале (ниже)

Справочная документация