diff --git a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts index ef4f3ef8355..bddd7d1d318 100644 --- a/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts +++ b/packages/runtime-core/__tests__/rendererOptimizedMode.spec.ts @@ -23,6 +23,7 @@ import { createApp } from '@vue/runtime-test' import { PatchFlags, SlotFlags } from '@vue/shared' +import { SuspenseImpl } from '../src/components/Suspense' describe('renderer: optimized mode', () => { let root: TestElement @@ -784,4 +785,40 @@ describe('renderer: optimized mode', () => { await nextTick() expect(inner(root)).toBe('
loading
') }) + + // #3828 + test('patch Suspense in optimized mode w/ nested dynamic nodes', async () => { + const show = ref(false) + + const app = createApp({ + render() { + return ( + openBlock(), + createBlock( + Fragment, + null, + [ + (openBlock(), + createBlock(SuspenseImpl, null, { + default: withCtx(() => [ + createVNode('div', null, [ + createVNode('div', null, show.value, PatchFlags.TEXT) + ]) + ]), + _: SlotFlags.STABLE + })) + ], + PatchFlags.STABLE_FRAGMENT + ) + ) + } + }) + + app.mount(root) + expect(inner(root)).toBe('
false
') + + show.value = true + await nextTick() + expect(inner(root)).toBe('
true
') + }) }) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 7510d78710a..d5d544469f3 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -1,9 +1,12 @@ import { VNode, normalizeVNode, - VNodeChild, VNodeProps, - isSameVNodeType + isSameVNodeType, + openBlock, + closeBlock, + currentBlock, + createVNode } from '../vnode' import { isFunction, isArray, ShapeFlags, toNumber } from '@vue/shared' import { ComponentInternalInstance, handleSetupResult } from '../component' @@ -79,7 +82,8 @@ export const SuspenseImpl = { } }, hydrate: hydrateSuspense, - create: createSuspenseBoundary + create: createSuspenseBoundary, + normalize: normalizeSuspenseChildren } // Force-casted public typing for h and TSX props inference @@ -709,31 +713,34 @@ function hydrateSuspense( /* eslint-enable no-restricted-globals */ } -export function normalizeSuspenseChildren( - vnode: VNode -): { - content: VNode - fallback: VNode -} { +function normalizeSuspenseChildren(vnode: VNode) { const { shapeFlag, children } = vnode - let content: VNode - let fallback: VNode - if (shapeFlag & ShapeFlags.SLOTS_CHILDREN) { - content = normalizeSuspenseSlot((children as Slots).default) - fallback = normalizeSuspenseSlot((children as Slots).fallback) - } else { - content = normalizeSuspenseSlot(children as VNodeChild) - fallback = normalizeVNode(null) - } - return { - content, - fallback - } + const isSlotChildren = shapeFlag & ShapeFlags.SLOTS_CHILDREN + vnode.ssContent = normalizeSuspenseSlot( + isSlotChildren ? (children as Slots).default : children + ) + vnode.ssFallback = isSlotChildren + ? normalizeSuspenseSlot((children as Slots).fallback) + : createVNode(Comment) } function normalizeSuspenseSlot(s: any) { + let block: VNode[] | null | undefined if (isFunction(s)) { + const isCompiledSlot = s._c + if (isCompiledSlot) { + // disableTracking: false + // allow block tracking for compiled slots + // (see ./componentRenderContext.ts) + s._d = false + openBlock() + } s = s() + if (isCompiledSlot) { + s._d = true + block = currentBlock + closeBlock() + } } if (isArray(s)) { const singleChild = filterSingleRoot(s) @@ -742,7 +749,11 @@ function normalizeSuspenseSlot(s: any) { } s = singleChild } - return normalizeVNode(s) + s = normalizeVNode(s) + if (block) { + s.dynamicChildren = block.filter(c => c !== s) + } + return s } export function queueEffectWithSuspense( diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 99adae44c01..7109287d011 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -26,8 +26,7 @@ import { AppContext } from './apiCreateApp' import { SuspenseImpl, isSuspense, - SuspenseBoundary, - normalizeSuspenseChildren + SuspenseBoundary } from './components/Suspense' import { DirectiveBinding } from './directives' import { TransitionHooks } from './components/BaseTransition' @@ -186,7 +185,7 @@ export interface VNode< // structure would be stable. This allows us to skip most children diffing // and only worry about the dynamic nodes (indicated by patch flags). export const blockStack: (VNode[] | null)[] = [] -let currentBlock: VNode[] | null = null +export let currentBlock: VNode[] | null = null /** * Open a block. @@ -452,9 +451,7 @@ function _createVNode( // normalize suspense children if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { - const { content, fallback } = normalizeSuspenseChildren(vnode) - vnode.ssContent = content - vnode.ssFallback = fallback + ;(type as typeof SuspenseImpl).normalize(vnode) } if (