Vueソースコード分析、keep-aliveはどのようにキャッシュを実装しますか?

序文

パフォーマンスの最適化に関して、最も一般的な方法はキャッシュです。要求または初期化プロセスを削減するために頻繁にアクセスする必要があるリソースをキャッシュし、それによって時間またはメモリ消費を削減します。Vue は、ルーティングレベルまたはコンポーネントレベルのキャッシュに使用できるキャッシュコンポーネントのキープアライブを提供します。

しかし、キャッシュの原則を知っていますか?コンポーネントキャッシュレンダリングはどのように機能しますか?次に、この記事では、キープアライブの原則を分析します。

 

LRU戦略

keep-aliveを使用する場合、prop属性include、exclude、およびmaxを追加して、コンポーネントを条件付きでキャッシュできるようにすることができます。制限があるため、古いコンポーネントはキャッシュを削除し、新しいコンポーネントを最新のキャッシュに追加する必要があります。対応する戦略を策定するにはどうすればよいですか。

履歴アクセスレコードデータを削除するためのデータ基づくLRU(最近使用されていない、最も最近使用されていない)ポリシーLRU戦略の設計原則は、データの一部が最近の期間にアクセスされていない場合、将来アクセスされる可能性は低いということです。つまり、限られたスペースがデータでいっぱいなると、データにアクセスできない最長のスペース排除されます。

  1. 現在、キャッシュは最大3つのコンポーネントしか格納できず、ABCの3つのコンポーネントが問題なく順番にキャッシュに入ります。
  2. Dコンポーネントにアクセスすると、メモリスペースが不足し、Aが最も古く、最も古いコンポーネントであるため、Aコンポーネントがキャッシュから削除され、Dコンポーネントが最新の位置に追加されます。
  3. コンポーネントBに再度アクセスすると、Bはまだキャッシュ内にあるため、Bは最新の位置に移動し、他のコンポーネントは次の位置に移動します。
  4. Eコンポーネントにアクセスすると、メモリスペースが不足し、Cが最も使用頻度の低いコンポーネントになり、Cコンポーネントがキャッシュから削除され、Eコンポーネントが最新の位置に追加されます。

キープアライブキャッシュメカニズムは、LRU戦略に従ってキャッシュコンポーネントの鮮度を設定し、長期間アクセスされていないコンポーネントをキャッシュから削除することです。キャッシュメカニズムを理解したら、ソースコードを入力して、キープアライブコンポーネントがどのように実装されているかを確認します。

 

コンポーネント実現の原則

// 源码位置: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が呼び出されて、親が抽象コンポーネントであるかどうかが判別されます。抽象コンポーネントの場合は、抽象コンポーネントおよび子コンポーネントとの階層関係を無視して、抽象コンポーネントの上位レベルが親として選択されます。

キープアライブコンポーネントに戻ると、コンポーネントは
テンプレートを書き込みませんが、レンダリング 関数がレンダリング結果を決定します。

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

keep-aliveに複数の子要素がある場合、keep-aliveでは同時にレンダリングする必要がある子要素は1つだけです。したがって、最初に、スロット内の子要素が取得され、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

一致条件は、キャッシュメカニズムのロジックに入ります。キャッシュにヒットした場合は、キャッシュされたインスタンスをキャッシュから取得して現在のコンポーネントに設定し、キーの位置を調整して最後に配置します。キャッシュが欠落している場合は、現在のVNodeをキャッシュし、現在のコンポーネントのキーを追加します。キャッシュコンポーネントの数が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メソッドを呼び出して、コンポーネントインスタンスを破棄し、キャッシュコンポーネントを空にして、対応するキーを削除します。

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は、マウントされたインクルードとエクスクルードの変更を監視し、属性が変更されたときにキャッシュとキーの順序を調整し、最後にpruneCacheEntryを呼び出します。

概要:キャッシュはコンポーネントのキャッシュに使用され、キーはコンポーネントのキーを格納します。キャッシュコンポーネントは、LRU戦略に従って調整されます。コンポーネントのVNodeはkeep-aliveのレンダリングで返されるため、keep-aliveは実際にはレンダリングされないが、レンダリングされたオブジェクトはパッケージのサブコンポーネントであるという結論を導き出すこともできます。

 

コンポーネントのレンダリングプロセス

ウォームリマインダー:コンテンツのこの部分では、レンダリングとパッチのプロセスを理解する必要があります

レンダリングプロセスの2つの主要なプロセスは、レンダリングとパッチです。レンダリングの前に、テンプレートのコンパイルが行われます。レンダリング 関数は、テンプレートのコンパイルの結果です。VNodeツリーの構築を担当します。構築されたVNodeはパッチに渡されます。パッチはVNodeの関係に基づいています。関係は実際のdomノードのツリーを生成します。

この図は、vue ビューレンダリングのプロセスを説明してい ます。

VNodeが構築された後、最終的には実際のdomに変換され、パッチは必要なプロセスです。コンポーネントのレンダリングプロセスをよりよく理解するために、keep-aliveにコンポーネントAとBが含まれ、コンポーネントAがデフォルトで表示されているとします。

 

初期レンダリング

コンポーネントは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
    }
  }
}

ある反応成分を再活性化されているかどうかivated識別します。レンダリングを初期化するとき、コンポーネントAは初期化および構築されておらず、componentInstanceはまだ定義されていません。コンポーネントAのkeepAliveはtrueです。これは、親パッケージコンポーネントとしてのkeep-aliveがコンポーネントAの前にマウントされるためです。つまり、kepp-aliveは最初にレンダリングプロセスを実行し、コンポーネントAがキャッシュされ、次に最初のコンポーネントがキャッシュされます。スロット内のがキャッシュされます。コンポーネント(コンポーネントA)のkeepAlive値はtrueに設定されます。このプロセスを覚えていない場合は、上記のコンポーネントによって実装されたコードを参照してくださいしたがって、この時点で反応するのは誤りです。

次に、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では、新しい 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を実際のdomに割り当て、次にinsertを呼び出してコンポーネントの実際のdomを親要素に挿入します。

したがって、最初のレンダリングでは、keep-aliveはAコンポーネントをキャッシュしてから、Aコンポーネントを通常どおりにレンダリングします。

 

キャッシュされたレンダリング

コンポーネントBに切り替えてからコンポーネントAに戻すと、コンポーネントAはキャッシュにヒットし、再アクティブ化されます。

パッチプロセスを再度実行すると、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)
  }
  // ...
}

フック関数 prepatchpatchVnodeで呼び出されます。

// 源码位置: 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であることがわかります。最初にresolveSlotsを呼び出してキープアライブスロットを更新し、次に$ forceUpdateを呼び出してキープアライブを再レンダリングしてから、レンダリングを再度実行します。Aコンポーネントは初期化中にキャッシュされているため、keep-aliveはキャッシュされたAコンポーネントVNodeを直接返します。VNodeの準備ができたら、パッチフェーズに入ります。

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

Aコンポーネントは、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のみが呼び出されます。したがって、キャッシュコンポーネントがアクティブ化されると、作成およびマウントされたライフサイクル関数は実行されません

createComponentに戻ります。この時点で、isReactivateは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が再レンダリングします。このようにして、レンダリング時に最新のコンポーネントが取得され、VNodeがキャッシュにヒットすると、キャッシュからVNodeが返されます。

 

総括する

キープアライブコンポーネントは抽象コンポーネントであり、親子関係に対応する場合は抽象コンポーネントをスキップします。パッケージ化された子コンポーネントのみを処理します。主にLRU戦略に従ってコンポーネントVNodeをキャッシュし、最後にのVNodeを返します。レンダリング中の子コンポーネント。キャッシュレンダリングプロセスは、キープアライブスロットを更新し、再度レンダリングし、キャッシュから前のコンポーネントVNodeを読み取って、状態キャッシュを実装します。

おすすめ

転載: blog.csdn.net/weixin_43844696/article/details/108405111