Sfoglia il codice sorgente

fix(templateRef): set ref on cached async component which wrapped in KeepAlive (#12290)

close #4999
close #5004
edison 1 anno fa
parent
commit
983eb50a17

+ 66 - 0
packages/runtime-core/__tests__/rendererTemplateRef.spec.ts

@@ -1,4 +1,6 @@
 import {
 import {
+  KeepAlive,
+  defineAsyncComponent,
   defineComponent,
   defineComponent,
   h,
   h,
   nextTick,
   nextTick,
@@ -538,4 +540,68 @@ describe('api: template refs', () => {
       '<div><div>[object Object],[object Object]</div><ul><li>2</li><li>3</li></ul></div>',
       '<div><div>[object Object],[object Object]</div><ul><li>2</li><li>3</li></ul></div>',
     )
     )
   })
   })
+
+  test('with async component which nested in KeepAlive', async () => {
+    const AsyncComp = defineAsyncComponent(
+      () =>
+        new Promise(resolve =>
+          setTimeout(() =>
+            resolve(
+              defineComponent({
+                setup(_, { expose }) {
+                  expose({
+                    name: 'AsyncComp',
+                  })
+                  return () => h('div')
+                },
+              }) as any,
+            ),
+          ),
+        ),
+    )
+
+    const Comp = defineComponent({
+      setup(_, { expose }) {
+        expose({
+          name: 'Comp',
+        })
+        return () => h('div')
+      },
+    })
+
+    const toggle = ref(false)
+    const instanceRef = ref<any>(null)
+
+    const App = {
+      render: () => {
+        return h(KeepAlive, () =>
+          toggle.value
+            ? h(AsyncComp, { ref: instanceRef })
+            : h(Comp, { ref: instanceRef }),
+        )
+      },
+    }
+
+    const root = nodeOps.createElement('div')
+    render(h(App), root)
+    expect(instanceRef.value.name).toBe('Comp')
+
+    // switch to async component
+    toggle.value = true
+    await nextTick()
+    expect(instanceRef.value).toBe(null)
+
+    await new Promise(r => setTimeout(r))
+    expect(instanceRef.value.name).toBe('AsyncComp')
+
+    // switch back to normal component
+    toggle.value = false
+    await nextTick()
+    expect(instanceRef.value.name).toBe('Comp')
+
+    // switch to async component again
+    toggle.value = true
+    await nextTick()
+    expect(instanceRef.value.name).toBe('AsyncComp')
+  })
 })
 })

+ 13 - 3
packages/runtime-core/src/rendererTemplateRef.ts

@@ -15,7 +15,7 @@ import { isRef, toRaw } from '@vue/reactivity'
 import { ErrorCodes, callWithErrorHandling } from './errorHandling'
 import { ErrorCodes, callWithErrorHandling } from './errorHandling'
 import type { SchedulerJob } from './scheduler'
 import type { SchedulerJob } from './scheduler'
 import { queuePostRenderEffect } from './renderer'
 import { queuePostRenderEffect } from './renderer'
-import { getComponentPublicInstance } from './component'
+import { type ComponentOptions, getComponentPublicInstance } from './component'
 import { knownTemplateRefs } from './helpers/useTemplateRef'
 import { knownTemplateRefs } from './helpers/useTemplateRef'
 
 
 /**
 /**
@@ -42,8 +42,18 @@ export function setRef(
   }
   }
 
 
   if (isAsyncWrapper(vnode) && !isUnmount) {
   if (isAsyncWrapper(vnode) && !isUnmount) {
-    // when mounting async components, nothing needs to be done,
-    // because the template ref is forwarded to inner component
+    // #4999 if an async component already resolved and cached by KeepAlive,
+    // we need to set the ref to inner component
+    if (
+      vnode.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE &&
+      (vnode.type as ComponentOptions).__asyncResolved &&
+      vnode.component!.subTree.component
+    ) {
+      setRef(rawRef, oldRawRef, parentSuspense, vnode.component!.subTree)
+    }
+
+    // otherwise, nothing needs to be done because the template ref
+    // is forwarded to inner component
     return
     return
   }
   }