ホームページ  >  記事  >  ウェブフロントエンド  >  Vue の KeepAlive コンポーネントについて説明する記事

Vue の KeepAlive コンポーネントについて説明する記事

青灯夜游
青灯夜游転載
2022-11-14 20:21:131785ブラウズ

Vue の KeepAlive コンポーネントについて説明する記事

最近、Vue 関連のナレッジポイントを調べていたところ、KeepAlive コンポーネントを見て、コンポーネント間の切り替え時に再レンダリングが行われない仕組みに興味があったので、もっと詳しく見てみましょう。 (学習ビデオ共有: vue ビデオ チュートリアル)

特定の内部実装がどのように実装されているかを知りたい場合、またはある程度の理解はあるものの十分に詳しくない場合は、次のことができます。

ヒントの統合: このようにして、面接中にこの知識ポイントについて他の人に大声で質問できますか?

KeepAlive とは

<keepalive></keepalive> は組み込みコンポーネントであり、その機能は 複数のコンポーネント間を動的に切り替えることです。 いつキャッシュ削除されたコンポーネント インスタンス。

KeepAlive 関数

KeepAlive という言葉は HTTP プロトコルから借用されたものです。HTTP プロトコルでは、KeepAlive は永続接続とも呼ばれます。その機能は複数のリクエストを許可することです。同じ HTTP 接続を共有する /responses により、HTTP 接続の頻繁な破棄と作成によって生じる追加のパフォーマンス オーバーヘッドが解決されます。同様に、Vue の KeepAlive コンポーネントも、 コンポーネントが頻繁に破棄/再構築されることを回避し、 パフォーマンスのオーバーヘッドを回避することを目的としています。

// App.vue
<Test :msg="curTab" v-if="curTab === &#39;Test&#39;"></Test>
<HelloWorld :msg="curTab" v-if="curTab === &#39;HelloWorld&#39;"></HelloWorld>
<div @click="toggle">toggle</div>

上記のコードからわかるように、トグルを頻繁にクリックすると、Test/HelloWorld コンポーネントが頻繁にレンダリングされます。ユーザーが頻繁にクリックすると、Test コンポーネントを頻繁に破棄/レンダリングする必要があります。レンダリング時間が長くなり、パフォーマンスが低下します。

したがって、このパフォーマンスのオーバーヘッドを解決するには、KeepAlive コンポーネントをいつ使用するかを知る必要があります。

<KeepAlive>
  <component :is="curTab === &#39;Test&#39; ? Test : HelloWorld" :msg="curTab"></component>
</KeepAlive>
<div @click="toggle">toggle</div>

この画面記録を見ることができます。最初のロード後、頻繁に切り替えても破壊されて再マウントされることはなく、コンポーネントが (破壊されるのではなく) 非アクティブ化されるだけです。レンダリングするコンポーネントが大きい場合は、パフォーマンスを最適化できます。

実際に体験してみたい場合は、この例をご覧ください。公式デモでは、データがキャッシュされますが、開発中および使用中に注意する必要があります

実装方法

実装原理は実際には非常に単純で、実際には、キャッシュ管理と特定の破棄およびレンダリング ロジックです。他のコンポーネントとは異なります。

KeepAlive コンポーネントは、コンポーネントを アンインストールするときに実際にアンインストールすることはできませんが、非表示のコンテナ に配置し、アクティブ化されると 非表示のコンテナからコンポーネントを削除します。それを取り出して、これを実際の dom にマウントします。これは、KeepAlive activateddeactivated の 2 つの固有のライフ サイクルに対応します。

Vue の KeepAlive コンポーネントについて説明する記事

まず、コンポーネントのマウント プロセスを簡単に理解しましょう

KeepAlive 固有のレンダリング ロジックの Vue の KeepAlive コンポーネントについて説明する記事 サブコンポーネントは、マウントおよびアンマウント中に実行されるため、マウントおよび破棄ロジックは実行されません。

#特定の実装 (小規模でシンプルな KeepAlive の実装)

    KeepAlive コンポーネントのプロパティ
  • const KeepAliveImpl: ComponentOptions = {
      name: "KeepAlive",
      // 标识这是一个 KeepAlive 组件
      __isKeepAlive: true,
      // props
      props: {
        exclude: [String, Array, RegExp],
        include: [String, Array, RegExp],
        max: [String, Number]
      }
     }
     
     // isKeepAlive
     export const isKeepAlive = (vnode: VNode): boolean =>
      (vnode.type as any).__isKeepAlive
#KeepAlive コンポーネントのセットアップ ロジックとレンダリング ロジック (注目)
  • // setup 接着上面的代码
    // 获取到当前 KeepAlive 组件实例
    const instance = getCurrentInstance()! as any;
    // 拿到 ctx
    const sharedContext = instance.ctx as KeepAliveContext;
    // cache 缓存
    // key: vnode.key | vnode.type value: vnode
    const cache: Cache = new Map()
    // 需要拿到某些的 renderer 操作函数,需要自己特定执行渲染和卸载逻辑
    const { renderer: { p: patch, m: move, um: _unmount, o: { createElement } } } = sharedContext
    // 隐藏的容器,用来存储需要隐藏的 dom
    const storeageContainer = createElement(&#39;div&#39;)
    
    // 存储当前的子组件的缓存 key
    let pendingKey: CacheKey | null = null
    
    sharedContext.activate = (vnode, container, anchor) => {
      // KeepAlive 下组件激活时执行的 move 逻辑
      move(vnode, container, anchor, 0 /* ENTER */)
    }
    
    sharedContext.deactivate = (vnode) => {
      // KeepAlive 下组件失活时执行的 move 逻辑
      move(vnode, storeageContainer, null, 1 /* LEAVE */)
    }
    
    return () => {
      // 没有子组件
      if (!slots.default) {
        return null;
      }
      const children = slots.default() as VNode[];
      const rawNode = children[0];
      let vnode = rawNode;
      const comp = vnode.type as ConcreteComponent;
      const name = comp.displayName || comp.name
      const { include, exclude } = props;
      // 没有命中的情况
      if (
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        // 直接渲染子组件
        return rawNode;
      }
      // 获取子组件的 vnode key
      const key = vnode.key == null ? comp : vnode.key;
      // 获取子组件缓存的 vnode
      const cachedVNode = cache.get(key);
    
      pendingKey = key;
      // 命中缓存
      if (cachedVNode) {
        vnode.el = cachedVNode.el;
        // 继承组件实例
        vnode.component = cachedVNode.component;
        // 在 vnode 上更新 shapeFlag,标记为 COMPONENT_KEPT_ALIVE 属性,防止渲染器重新挂载
        vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
      } else {
        // 没命中将其缓存
        cache.set(pendingKey, vnode)
      }
      // 在 vnode 上更新 shapeFlag,标记为 COMPONENT_SHOULD_KEEP_ALIVE 属性,防止渲染器将组件卸载了
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
      // 渲染组件 vnode
      return vnode;
    }

KeepAlive コンポーネントがマウントされると、レンダラーは ctx にマウントされます。
  • ##KeepAlive コンポーネントでは、sharedContext 上のレンダラーから取得されます。 move、createElement など。

    function mountComponent() {
     // ...
     if (isKeepAlive(initialVNode)) {
        ;(instance.ctx as KeepAliveContext).renderer = internals
      }
    }

サブコンポーネントは特定の破棄およびレンダリング ロジックを実行します

  • まず、ご覧のとおり、上記の

    KeepAlive コンポーネントをレンダリングするとき、対応するshapeFlag フラグがサブコンポーネントの vnode に追加されます。コンポーネントはマウントされていますが、これにはマウントが必要ありません。特別な処理が必要です
  • const processComponent = (
        n1: VNode | null,
        n2: VNode,
        container: RendererElement,
        anchor: RendererNode | null,
      ) => {
        if (n1 == null) {
          // 在 KeepAlive 组件渲染时会对子组件增加 COMPONENT_KEPT_ALIVE 标志
          // 挂载子组件时会判断是否 COMPONENT_KEPT_ALIVE ,如果是不会调用 mountComponent 而是直接执行 activate 方法
          if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
            ;(parentComponent!.ctx as KeepAliveContext).activate(
              n2,
              container,
              anchor
            )
          }
          // ...
        }
      }
同様に

COMPONENT_SHOULD_KEEP_ALIVEフラグは、コンポーネントがアンマウントされるときに、アンマウントが必要ではないことをレンダラーに伝えるためにも使用されます。特殊な加工が必要となります。

const unmount: UnmountFn = (vnode) => {
  // ...
  // 在 KeepAlive 组件渲染时会对子组件增加 COMPONENT_SHOULD_KEEP_ALIVE 标志
  // 然后在子组件卸载时并不会真实的卸载而是调用 KeepAlive 的 deactivate 方法
  if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
    ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
    return
  }
}

activated

および

deactivated ライフ サイクルをマウントする方法 (ライフ サイクル関連のものに注目する必要はありません)

  • まず、これら 2 つのライフ サイクルは KeepAlive コンポーネント内で一意に宣言され、使用するために直接エクスポートされます。

    export function onActivated(
      hook: Function,
      target?: ComponentInternalInstance | null
    ) {
      // 注册 activated 的回调函数到当前的 instance 的钩子函数上
      registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target)
    }
    export function onDeactivated(
      hook: Function,
      target?: ComponentInternalInstance | null
    ) {
      // 注册 deactivated 的回调函数到当前的 instance 的钩子函数上
      registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target)
    }

    然后因为这两个生命周期会注册在 setup 里面,所以只要执行 setup 就会将两个生命周期的回调函数注册到当前的 instance 实例上

    // renderer.ts
    // mount 函数逻辑
    const mountComponent = (initialVNode,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      optimized
    ) => {
      // ...
      const instance: ComponentInternalInstance =
        compatMountInstance ||
        (initialVNode.component = createComponentInstance(
        initialVNode,
        parentComponent,
        parentSuspense
      ))
      // 执行 setup
      setupComponent(instance)
    }
    // setupcomponent 处理 setup 函数值
    export function setupComponent(
      instance: ComponentInternalInstance,
      isSSR = false
    ) {
      // ...
      const isStateful = isStatefulComponent(instance)
      // ...
      const setupResult = isStateful
        // setupStatefulComponent 函数主要功能是设置当前的 instance
        ? setupStatefulComponent(instance, isSSR)
        : undefined
      // ...
    }
    
    function setupStatefulComponent(
      instance: ComponentInternalInstance
    ){
      if (setup) {
        //设置当前实例
        setCurrentInstance(instance)
        // 执行组件内 setup 函数,执行 onActivated 钩子函数进行回调函数收集
        const setupResult = callWithErrorHandling(
          setup,
          instance,
          ErrorCodes.SETUP_FUNCTION,
          [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
        )
        // currentInstance = null;
        unsetCurrentInstance()
      }
    }

    最后在执行sharedContext.activatesharedContext.deactivate的时候将注册在实例上的回调函数取出来直接执行就OK了,执行时机在 postRender 之后

    sharedContext.activate = (vnode, container, anchor) => {
      // KeepAlive 下组件激活时执行的 move 逻辑
      move(vnode, container, anchor, 0 /* ENTER */)
      // 把回调推入到 postFlush 的异步任务队列中去执行
      queuePostRenderEffect(() => {
        if (instance.a) {
          // a是 activated 钩子的简称
          invokeArrayFns(instance.a)
        }
      })
    }
    sharedContext.activate = (vnode, container, anchor) => {
      // KeepAlive 下组件失活时执行的 move 逻辑
      move(vnode, container, anchor, 0 /* ENTER */)
      queuePostRenderEffect(() => {
        if (instance.da) {
          // da是 deactivated 钩子的简称
          invokeArrayFns(instance.da)
        }
      })
    }
    
    export const enum LifecycleHooks {
      // ... 其他生命周期声明
      DEACTIVATED = &#39;da&#39;,
      ACTIVATED = &#39;a&#39;,
    }
    export interface ComponentInternalInstance {
    // ... 其他生命周期
    [LifecycleHooks.ACTIVATED]: Function[]
    [LifecycleHooks.DEACTIVATED]: Function[]
    }

    以下是关于上述demo如何实现的简化流程图

    Vue の KeepAlive コンポーネントについて説明する記事

    需要注意的知识点

    1、什么时候缓存

    KeepAlive 组件的onMountedonUpdated生命周期时进行缓存

    2、什么时候取消缓存

    • 缓存数量超过设置的 max 时

    • 监听 include 和 exclude 修改的时候,会读取缓存中的知进行判断是否需要清除缓存

    修剪缓存的时候也要 unmount(如果该缓存不是当前组件)或者 resetShapeFlag 将标志为从 KeepAlive 相关 shapeFlag 状态重置为 STATEFUL_COMPONENT 状态(如果该缓存是当前组件,但是被exclude了),当然 unmount 函数内包含 resetShapeFlag 操作

    3、缓存策略

    KeepAlive 组件的缓存策略是 LRU(last recently used)缓存策略

    核心思想在于需要把当前访问或渲染的组件作为最新一次渲染的组件,并且该组件在缓存修剪过程中始终是安全的,即不会被修剪。

    看下面的图更加直观,图片来源一篇讲keepAlive 缓存优化的文章

    4、如何添加到 vue devtools 组件树上

    sharedContext.activate = (vnode, container, anchor) => {
      // instance 是子组件实例
      const instance = vnode.component!
      // ...
      // dev环境下设置, 自己模拟写的
      devtools.emit(&#39;component:added&#39;, instance.appContext.app, instance.uid, instance.parent ? instance.parent.uid: undefined, instance)
      // 官方添加
      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
        // Update components tree
        devtoolsComponentAdded(instance)
      }
    }
    // 同理 sharedContext.deactivates 上也要添加,不然不会显示在组件树上

    5、缓存的子组件 props 更新处理

    当子组件有 prop 更新时是需要重新去 patch 的,所以在 activate 的时候需要重新执行 patch 进行子组件更新

    sharedContext.activate = (vnode, container, anchor) => {
      // ...
      // props 改变需要重新 patch(update)
      patch(
        instance.vnode,
        vnode,
        container,
        anchor,
        instance,
        parentSuspense,
        isSVG,
        vnode.slotScopeIds,
        optimized
      )
    }

    (学习视频分享:web前端开发编程基础视频

以上がVue の KeepAlive コンポーネントについて説明する記事の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。