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('
')
})
+
+ // #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('')
+
+ show.value = true
+ await nextTick()
+ expect(inner(root)).toBe('')
+ })
})
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 (