Разбор исходного кода Vue, как поддержка активности реализует кэширование?

Vue.js

предисловие

С точки зрения оптимизации производительности наиболее распространенным средством является кэширование. Кэшируйте часто используемые ресурсы, чтобы уменьшить количество запросов или процессов инициализации, тем самым сокращая время или потребление памяти.VueПредоставляет нам компонент кешаkeep-alive, который можно использовать для кэширования на уровне маршрута или на уровне компонентов.

Но понимаете ли вы принцип кэширования и как работает рендеринг кэша компонентов? Тогда эта статья будет анализироватьkeep-aliveпринцип.

Стратегия ЛРУ

В использованииkeep-alive, можете добавитьpropАтрибутыinclude,exclude,maxПозволяет условное кэширование компонентов. Так как есть ограничения, старые компоненты нужно удалять из кеша, а новые компоненты нужно добавлять в последний кеш, так как же сформулировать соответствующую стратегию?

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

  1. Теперь кэшу разрешено хранить до 3 компонентов, и три компонента ABC без проблем входят в кэш по очереди.
  2. При доступе к компоненту D недостаточно места в памяти, компонент A является самым ранним и самым старым компонентом, поэтому компонент A удаляется из кеша, а компонент D добавляется в последнюю позицию.
  3. Когда к компоненту B обращаются снова, поскольку B все еще находится в кэше, B перемещается в последнюю позицию, а другие компоненты соответственно возвращаются на один бит назад.
  4. При доступе к компоненту E недостаточно места в памяти, C становится компонентом, который не использовался дольше всего, компонент C удаляется из кеша, а компонент E добавляется на последнюю позицию

keep-aliveМеханизм кэширования заключается в установке свежести компонентов кэша в соответствии со стратегией LRU и удалении из кэша компонентов, к которым в течение длительного времени не обращались. Разобравшись с механизмом кэширования, перейдите к исходному коду и посмотритеkeep-aliveКак реализованы компоненты.

Принцип реализации компонента

// 源码位置:src/core/components/keep-alive.js
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
    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
        // 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)
      } 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])
  }
}

kepp-aliveНа самом деле это абстрактный компонент, который обрабатывает только обернутые подкомпоненты и не будет устанавливать отношения родитель-потомок с подкомпонентами, а также не будет отображаться на странице как узел. установить в начале компонентаabstractдляtrue, указывающий, что компонент является абстрактным компонентом.

// 源码位置: src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
  vm.$parent = parent
  // ...
}

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

назадkeep-aliveкомпоненты, компоненты не написаныtemplateшаблон, но поrenderФункция определяет результат рендеринга.

const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)

еслиkeep-aliveесть несколько дочерних элементов,keep-aliveТребуется, чтобы одновременно отображался только один дочерний элемент. Итак, в начале получим дочерние элементы в слоте, вызовитеgetFirstComponentChildполучить первый дочерний элементVNode.

// 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
}

function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
  if (Array.isArray(pattern)) {
    return pattern.indexOf(name) > -1
  } else if (typeof pattern === 'string') {
    return pattern.split(',').indexOf(name) > -1
  } else if (isRegExp(pattern)) {
    return pattern.test(name)
  }
  return false
}

Затем определите, соответствует ли текущий компонент условиям кэширования и совпадает ли имя компонента с именем компонента.includeне совпадает или не совпадаетexcludeМатчи будут выходить напрямую и возвращатьсяVNode, не используйте механизм кэширования.

const { cache, keys } = this
const key: ?string = vnode.key == null
  // 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)
} 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

Условие соответствия передает логику, которая войдет в механизм кеша.cacheПолучите кэшированный экземпляр, установленный на текущий компонент, и настройтеkeyположение, чтобы поставить его в последнюю очередь. Если кэш не попал, текущийVNodeКэшируется и добавляется к текущему компонентуkey.如果缓存组件的数量超出maxЗначение , то есть места в кеше недостаточно, то вызовитеpruneCacheEntryУдалить самый старый компонент из кеша, т.е.keys[0]с компонент. Затем компонентkeepAliveотметить какtrue, что указывает на то, что это кэшированный компонент.

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()
  }
  cache[key] = null
  remove(keys, key)
}

pruneCacheEntryОтвечает за удаление компонента из кеша, вызывает компонент$destroyМетод уничтожает экземпляр компонента, опустошивает кэшированный компонент и удаляет соответствующиеkey.

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

function pruneCache (keepAliveInstance: any, filter: Function) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      const name: ?string = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

keep-aliveсуществуетmountedбудет контролироватьincludeа такжеexcludeизменения, настроить кеш иkeysпорядок, финальный звонок тожеpruneCacheEntry.

резюме:cacheдля кэширования компонентов,keysкомпоненты храненияkey, настройте компоненты кэша в соответствии со стратегией LRU.keep-aliveизrenderнаконец вернет компонентVNode, так что мы также можем сделать вывод,keep-aliveДело не в том, что он не отображается, а в том, что отображаемый объект является обернутым дочерним компонентом.

Процесс рендеринга компонентов

Напоминание: эта часть контента должна быть правильнойrenderа такжеpatchзнать процесс

Два наиболее важных процесса в процессе рендеринга:renderа такжеpatch,существуетrenderПрежде чем появится компилятор шаблонов,renderФункция — это продукт компиляции шаблона, который отвечает за построениеVNodeдерево, построенноеVNodeбудет переданоpatch,patchсогласно сVNodeОтношение генерирует настоящее дерево узлов DOM.

Эта картина изображенаVueПосмотреть процесс рендеринга:

VNodeПосле того, как сборка будет завершена, он в конечном итоге будет преобразован в настоящий дом, иpatchявляется необходимым процессом. Чтобы лучше понять процесс рендеринга компонента, предположим,keep-aliveВключены компоненты A и B, а компонент A отображается по умолчанию.

Инициализировать рендеринг

компонент вpatchпроцесс будет выполненcreateComponentДля монтажа компонентов компонент A не является исключением.

// 源码位置: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
    }
  }
}

isReactivatedОпределяет, активирован ли компонент повторно. При инициализации рендеринга компонент A не был инициализирован и сконструирован.componentInstanceещеundefined. И компонент АkeepAliveдаtrue,потому чтоkeep-aliveКак компонент родительского пакета, он будет смонтирован перед компонентом A, т.е.kepp-aliveбудет выполняться первымrenderпроцесс, компонент A кэшируется, а затем первый компонент (компонент A) в слотеkeepAliveназначить какtrue, если вы не помните этот процесс, посмотрите код, реализованный вышеуказанными компонентами. Итак, в это времяisReactivatedдаfalse.

затем позвонитinitФункция выполняет инициализацию компонента, которая является перехватывающей функцией компонента:

// 源码位置: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)
    }
  },
  // ...
}

createComponentInstanceForVnodeвнутреннее собраниеnew VueСоздайте экземпляр компонента и назначьте егоcomponentInstance, затем позвоните$mountМонтировать компоненты.

назадcreateComponent, продолжайте со следующей логикой:

if (isDef(vnode.componentInstance)) {
  initComponent(vnode, insertedVnodeQueue)
  insert(parentElm, vnode.elm, refElm)
  if (isTrue(isReactivated)) {
    reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
  }
  return true
}

передачаinitComponentБудуvnode.elmПрисвойте значение реальному дому, а затем вызовитеinsertВставьте реальный дом компонента в родительский элемент.

Таким образом, в первоначальном рендерингеkeep-aliveКэшируйте компонент A, а затем визуализируйте компонент A в обычном режиме.

кэшированный рендеринг

При переключении на компонент B, а затем обратном переключении на компонент A кэш обращений компонента A повторно активируется.

испытать сноваpatchОбработать,keep-aliveТекущий компонент получается в соответствии со слотом, так как же обновляется содержимое слота для реализации кэша?

const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
  // patch existing root node
  patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}

При неинициализированном рендерингеpatchпозвонюpatchVnodeСравните старые и новые узлы.

// 源码位置:src/core/vdom/patch.js
function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // ...
  let i
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }
  // ...
}

patchVnodeФункция ловушки будет вызываться внутриprepatch.

// 源码位置: src/core/vdom/create-component.js
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  const options = vnode.componentOptions
  const child = vnode.componentInstance = oldVnode.componentInstance
  updateChildComponent(
    child,
    options.propsData, // updated props
    options.listeners, // updated listeners
    vnode, // new parent vnode
    options.children // new children
  )
},

updateChildComponentЭто ключевой метод обновления, который в основном обновляет некоторые свойства экземпляра:

// 源码位置:src/core/instance/lifecycle.js
export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode,
  renderChildren: ?Array<VNode>
) {
  // ...

  // Any static slot children from the parent may have changed during parent's
  // update. Dynamic scoped slots may also have changed. In such cases, a forced
  // update is necessary to ensure correctness.
  const needsForceUpdate = !!(
    renderChildren ||               // has new static slots
    vm.$options._renderChildren ||  // has old static slots
    hasDynamicScopedSlot
  )
  
  // ...
  
  // resolve slots + force update if has children
  if (needsForceUpdate) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }
}

Vue.prototype.$forceUpdate = function () {
  const vm: Component = this
  if (vm._watcher) {
    // 这里最终会执行 vm._update(vm._render)
    vm._watcher.update()
  }
}

Как видно из комментариевneedsForceUpdateтолько если есть слотtrue,keep-aliveСоответствовать критериям. первый звонокresolveSlotsвозобновитьkeep-aliveслот, затем позвоните$forceUpdateПозволятьkeep-aliveперерисуй, давай сноваrender. Поскольку компонент A был кэширован во время инициализации,keep-aliveВернуть кешированный компонент A напрямуюVNode.VNodeКогда готов, вот сноваpatchсцена.

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процесс, вызовinit.

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)
    }
  },
}

больше не пойду$mountЛогика, просто позвониprepatchОбновите свойства экземпляра. Поэтому, когда компонент кеша активирован, он не будет выполнятьсяcreatedа такжеmountedфункция жизненного цикла.

назадcreateComponent, В настоящее времяisReactivatedдляtrue,передачаreactivateComponent:

function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i
  // hack for #4339: a reactivated component with inner transition
  // does not trigger because the inner node's created hooks are not called
  // again. It's not ideal to involve module-specific logic in here but
  // there doesn't seem to be a better way to do it.
  let innerNode = vnode
  while (innerNode.componentInstance) {
    innerNode = innerNode.componentInstance._vnode
    if (isDef(i = innerNode.data) && isDef(i = i.transition)) {
      for (i = 0; i < cbs.activate.length; ++i) {
        cbs.activate[i](emptyNode, innerNode)
      }
      insertedVnodeQueue.push(innerNode)
      break
    }
  }
  // unlike a newly created component,
  // a reactivated keep-alive component doesn't insert itself
  insert(parentElm, vnode.elm, refElm)
}

последний звонокinsertВставьте узел dom компонента, и процесс рендеринга кеша завершен.

резюме: при первом рендеринге компонентаkeep-aliveКомпонент будет кэширован. Когда кеш рендерится,keep-aliveобновит содержимое слота после$forceUpdateРендеринг. так вrenderПолучить последний компонент, когда он попадет в кеш, и вернуть его из кеша, если он попадет в кешVNode.

Суммировать

keep-aliveКомпонент является абстрактным компонентом, и он будет пропускать абстрактный компонент при соответствии отношениям родитель-потомок.Он обрабатывает только обернутые дочерние компоненты, в основном кэшируя компоненты в соответствии со стратегией LRU.VNode, и, наконец, вrenderвозвращает дочерний компонентVNode. Кэшированный процесс рендеринга обновленkeep-aliveслот, реrenderодин раз прочитать предыдущий компонент из кешаVNodeРеализовать кэширование состояния.

Связанные прошлые статьи:

Рука об руку, чтобы помочь вам понять принцип отзывчивости Vue

Рука об руку, чтобы помочь вам понять вычислительный принцип Vue

Коснитесь своей руки, чтобы понять принцип часов Vue.

Механизм асинхронного обновления и принцип nextTick, которые вы должны знать о Vue

Анализ принципа рендеринга Vue View, от построения VNode до создания реального дерева узлов