Przeglądaj źródła

fix(runtime-core): avoid replaying keep-alive updates into inactive branches

daiwei 1 tydzień temu
rodzic
commit
4b5680a01f

+ 69 - 0
packages/runtime-core/__tests__/components/KeepAlive.spec.ts

@@ -26,6 +26,7 @@ import {
   shallowRef,
 } from '@vue/runtime-test'
 import type { KeepAliveProps } from '../../src/components/KeepAlive'
+import { queuePostFlushCb } from '../../src/scheduler'
 
 const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
 
@@ -431,6 +432,74 @@ describe('KeepAlive', () => {
     expect(serializeInner(root)).toBe(`<main>C</main>`)
   })
 
+  test('should keep deferred branch updates pending when re-activation is immediately reversed', async () => {
+    const mountedA = vi.fn()
+    const mountedB = vi.fn()
+    const visible = ref(true)
+
+    const A = defineComponent({
+      name: 'A',
+      setup() {
+        onMounted(mountedA)
+        return () => h('span', 'Comp A')
+      },
+    })
+
+    const B = defineComponent({
+      name: 'B',
+      setup() {
+        onMounted(mountedB)
+        return () => h('span', 'Comp B')
+      },
+    })
+
+    const comp = shallowRef(A)
+    const Home = defineComponent({
+      name: 'Home',
+      setup() {
+        return () => h('main', [h(KeepAlive, null, [h(comp.value)])])
+      },
+    })
+
+    const App = defineComponent({
+      setup() {
+        return () => h(KeepAlive, null, [visible.value ? h(Home) : null])
+      },
+    })
+
+    render(h(App), root)
+    expect(serializeInner(root)).toBe(`<main><span>Comp A</span></main>`)
+    expect(mountedA).toHaveBeenCalledTimes(1)
+    expect(mountedB).toHaveBeenCalledTimes(0)
+
+    visible.value = false
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<!---->`)
+
+    comp.value = B
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<!---->`)
+    expect(mountedB).toHaveBeenCalledTimes(0)
+
+    const deactivateAfterActivate = vi.fn(() => {
+      visible.value = false
+    }) as any
+    deactivateAfterActivate.id = -1
+
+    visible.value = true
+    queuePostFlushCb(deactivateAfterActivate)
+    await nextTick()
+
+    expect(serializeInner(root)).toBe(`<!---->`)
+    expect(deactivateAfterActivate).toHaveBeenCalledTimes(1)
+    expect(mountedB).toHaveBeenCalledTimes(0)
+
+    visible.value = true
+    await nextTick()
+    expect(serializeInner(root)).toBe(`<main><span>Comp B</span></main>`)
+    expect(mountedB).toHaveBeenCalledTimes(1)
+  })
+
   async function assertNameMatch(props: KeepAliveProps) {
     const outerRef = ref(true)
     const viewRef = ref('one')

+ 8 - 3
packages/runtime-core/src/components/KeepAlive.ts

@@ -43,7 +43,7 @@ import {
   queuePostRenderEffect,
   setKeepAliveBranchActive,
 } from '../renderer'
-import { queuePostFlushCb } from '../scheduler'
+import { type SchedulerJob, queueJob, queuePostFlushCb } from '../scheduler'
 import { setTransitionHooks } from './BaseTransition'
 import type { ComponentRenderContext } from '../componentPublicInstance'
 import { devtoolsComponentAdded } from '../devtools'
@@ -153,11 +153,16 @@ const KeepAliveImpl: ComponentOptions = {
         optimized,
       )
       if (updates) {
-        // Replay deferred child updates after the branch is active again.
+        // Replay deferred child updates through the scheduler after the branch
+        // is active again so parent jobs can still flip the branch back to
+        // inactive before child updates run.
         queuePostFlushCb(() => {
           for (const pending of updates) {
             if (!pending.isUnmounted) {
-              pending.update()
+              const job = (() => pending.update()) as SchedulerJob
+              job.id = pending.uid
+              job.i = pending
+              queueJob(job)
             }
           }
           updates.clear()