最近、Vue 関連のナレッジポイントを調べていたところ、KeepAlive コンポーネントを見て、コンポーネント間の切り替え時に再レンダリングが行われない仕組みに興味があったので、もっと詳しく見てみましょう。 (学習ビデオ共有: vue ビデオ チュートリアル)
特定の内部実装がどのように実装されているかを知りたい場合、またはある程度の理解はあるものの十分に詳しくない場合は、次のことができます。
ヒントの統合: このようにして、面接中にこの知識ポイントについて他の人に大声で質問できますか?
<keepalive></keepalive>
は組み込みコンポーネントであり、その機能は 複数のコンポーネント間を動的に切り替えることです。 いつキャッシュ削除されたコンポーネント インスタンス。
KeepAlive という言葉は HTTP プロトコルから借用されたものです。HTTP プロトコルでは、KeepAlive は永続接続とも呼ばれます。その機能は複数のリクエストを許可することです。同じ HTTP 接続を共有する /responses により、HTTP 接続の頻繁な破棄と作成によって生じる追加のパフォーマンス オーバーヘッドが解決されます。同様に、Vue の KeepAlive コンポーネントも、 コンポーネントが頻繁に破棄/再構築されることを回避し、 パフォーマンスのオーバーヘッドを回避することを目的としています。
// App.vue <Test :msg="curTab" v-if="curTab === 'Test'"></Test> <HelloWorld :msg="curTab" v-if="curTab === 'HelloWorld'"></HelloWorld> <div @click="toggle">toggle</div>
上記のコードからわかるように、トグルを頻繁にクリックすると、Test/HelloWorld コンポーネントが頻繁にレンダリングされます。ユーザーが頻繁にクリックすると、Test コンポーネントを頻繁に破棄/レンダリングする必要があります。レンダリング時間が長くなり、パフォーマンスが低下します。
したがって、このパフォーマンスのオーバーヘッドを解決するには、KeepAlive コンポーネントをいつ使用するかを知る必要があります。
<KeepAlive> <component :is="curTab === 'Test' ? Test : HelloWorld" :msg="curTab"></component> </KeepAlive> <div @click="toggle">toggle</div>
この画面記録を見ることができます。最初のロード後、頻繁に切り替えても破壊されて再マウントされることはなく、コンポーネントが (破壊されるのではなく) 非アクティブ化されるだけです。レンダリングするコンポーネントが大きい場合は、パフォーマンスを最適化できます。
実際に体験してみたい場合は、この例をご覧ください。公式デモでは、データがキャッシュされますが、開発中および使用中に注意する必要があります
実装原理は実際には非常に単純で、実際には、キャッシュ管理と特定の破棄およびレンダリング ロジックです。他のコンポーネントとは異なります。
KeepAlive コンポーネントは、コンポーネントを アンインストールするときに実際にアンインストールすることはできませんが、非表示のコンテナ に配置し、アクティブ化されると 非表示のコンテナからコンポーネントを削除します。それを取り出して、これを実際の dom にマウントします。これは、KeepAlive activated と
deactivated の 2 つの固有のライフ サイクルに対応します。
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
// 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('div') // 存储当前的子组件的缓存 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; }
function mountComponent() { // ... if (isKeepAlive(initialVNode)) { ;(instance.ctx as KeepAliveContext).renderer = internals } }
サブコンポーネントは特定の破棄およびレンダリング ロジックを実行します
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 ライフ サイクルをマウントする方法 (ライフ サイクル関連のものに注目する必要はありません)
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.activate
和sharedContext.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 = 'da', ACTIVATED = 'a', } export interface ComponentInternalInstance { // ... 其他生命周期 [LifecycleHooks.ACTIVATED]: Function[] [LifecycleHooks.DEACTIVATED]: Function[] }
以下是关于上述demo如何实现的简化流程图
KeepAlive 组件的onMounted
和onUpdated
生命周期时进行缓存
缓存数量超过设置的 max 时
修剪缓存的时候也要 unmount(如果该缓存不是当前组件)或者 resetShapeFlag 将标志为从 KeepAlive 相关 shapeFlag 状态重置为 STATEFUL_COMPONENT 状态(如果该缓存是当前组件,但是被exclude了),当然 unmount 函数内包含 resetShapeFlag 操作
KeepAlive 组件的缓存策略是 LRU(last recently used)缓存策略
核心思想在于需要把当前访问或渲染的组件作为最新一次渲染的组件,并且该组件在缓存修剪过程中始终是安全的,即不会被修剪。
看下面的图更加直观,图片来源一篇讲keepAlive 缓存优化的文章
sharedContext.activate = (vnode, container, anchor) => { // instance 是子组件实例 const instance = vnode.component! // ... // dev环境下设置, 自己模拟写的 devtools.emit('component:added', instance.appContext.app, instance.uid, instance.parent ? instance.parent.uid: undefined, instance) // 官方添加 if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { // Update components tree devtoolsComponentAdded(instance) } } // 同理 sharedContext.deactivates 上也要添加,不然不会显示在组件树上
当子组件有 prop 更新时是需要重新去 patch 的,所以在 activate 的时候需要重新执行 patch 进行子组件更新
sharedContext.activate = (vnode, container, anchor) => { // ... // props 改变需要重新 patch(update) patch( instance.vnode, vnode, container, anchor, instance, parentSuspense, isSVG, vnode.slotScopeIds, optimized ) }
以上がVue の KeepAlive コンポーネントについて説明する記事の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。