fix(suspense): fix suspense patching in optimized mode

fix #3828
This commit is contained in:
Evan You 2021-05-27 16:32:31 -04:00
parent f0eb1978b2
commit 9f24195d2c
3 changed files with 74 additions and 29 deletions

View File

@ -23,6 +23,7 @@ import {
createApp createApp
} from '@vue/runtime-test' } from '@vue/runtime-test'
import { PatchFlags, SlotFlags } from '@vue/shared' import { PatchFlags, SlotFlags } from '@vue/shared'
import { SuspenseImpl } from '../src/components/Suspense'
describe('renderer: optimized mode', () => { describe('renderer: optimized mode', () => {
let root: TestElement let root: TestElement
@ -784,4 +785,40 @@ describe('renderer: optimized mode', () => {
await nextTick() await nextTick()
expect(inner(root)).toBe('<div><div><span>loading</span></div></div>') expect(inner(root)).toBe('<div><div><span>loading</span></div></div>')
}) })
// #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('<div><div>false</div></div>')
show.value = true
await nextTick()
expect(inner(root)).toBe('<div><div>true</div></div>')
})
}) })

View File

@ -1,9 +1,12 @@
import { import {
VNode, VNode,
normalizeVNode, normalizeVNode,
VNodeChild,
VNodeProps, VNodeProps,
isSameVNodeType isSameVNodeType,
openBlock,
closeBlock,
currentBlock,
createVNode
} from '../vnode' } from '../vnode'
import { isFunction, isArray, ShapeFlags, toNumber } from '@vue/shared' import { isFunction, isArray, ShapeFlags, toNumber } from '@vue/shared'
import { ComponentInternalInstance, handleSetupResult } from '../component' import { ComponentInternalInstance, handleSetupResult } from '../component'
@ -79,7 +82,8 @@ export const SuspenseImpl = {
} }
}, },
hydrate: hydrateSuspense, hydrate: hydrateSuspense,
create: createSuspenseBoundary create: createSuspenseBoundary,
normalize: normalizeSuspenseChildren
} }
// Force-casted public typing for h and TSX props inference // Force-casted public typing for h and TSX props inference
@ -709,31 +713,34 @@ function hydrateSuspense(
/* eslint-enable no-restricted-globals */ /* eslint-enable no-restricted-globals */
} }
export function normalizeSuspenseChildren( function normalizeSuspenseChildren(vnode: VNode) {
vnode: VNode
): {
content: VNode
fallback: VNode
} {
const { shapeFlag, children } = vnode const { shapeFlag, children } = vnode
let content: VNode const isSlotChildren = shapeFlag & ShapeFlags.SLOTS_CHILDREN
let fallback: VNode vnode.ssContent = normalizeSuspenseSlot(
if (shapeFlag & ShapeFlags.SLOTS_CHILDREN) { isSlotChildren ? (children as Slots).default : children
content = normalizeSuspenseSlot((children as Slots).default) )
fallback = normalizeSuspenseSlot((children as Slots).fallback) vnode.ssFallback = isSlotChildren
} else { ? normalizeSuspenseSlot((children as Slots).fallback)
content = normalizeSuspenseSlot(children as VNodeChild) : createVNode(Comment)
fallback = normalizeVNode(null)
}
return {
content,
fallback
}
} }
function normalizeSuspenseSlot(s: any) { function normalizeSuspenseSlot(s: any) {
let block: VNode[] | null | undefined
if (isFunction(s)) { 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() s = s()
if (isCompiledSlot) {
s._d = true
block = currentBlock
closeBlock()
}
} }
if (isArray(s)) { if (isArray(s)) {
const singleChild = filterSingleRoot(s) const singleChild = filterSingleRoot(s)
@ -742,7 +749,11 @@ function normalizeSuspenseSlot(s: any) {
} }
s = singleChild s = singleChild
} }
return normalizeVNode(s) s = normalizeVNode(s)
if (block) {
s.dynamicChildren = block.filter(c => c !== s)
}
return s
} }
export function queueEffectWithSuspense( export function queueEffectWithSuspense(

View File

@ -26,8 +26,7 @@ import { AppContext } from './apiCreateApp'
import { import {
SuspenseImpl, SuspenseImpl,
isSuspense, isSuspense,
SuspenseBoundary, SuspenseBoundary
normalizeSuspenseChildren
} from './components/Suspense' } from './components/Suspense'
import { DirectiveBinding } from './directives' import { DirectiveBinding } from './directives'
import { TransitionHooks } from './components/BaseTransition' import { TransitionHooks } from './components/BaseTransition'
@ -186,7 +185,7 @@ export interface VNode<
// structure would be stable. This allows us to skip most children diffing // structure would be stable. This allows us to skip most children diffing
// and only worry about the dynamic nodes (indicated by patch flags). // and only worry about the dynamic nodes (indicated by patch flags).
export const blockStack: (VNode[] | null)[] = [] export const blockStack: (VNode[] | null)[] = []
let currentBlock: VNode[] | null = null export let currentBlock: VNode[] | null = null
/** /**
* Open a block. * Open a block.
@ -452,9 +451,7 @@ function _createVNode(
// normalize suspense children // normalize suspense children
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
const { content, fallback } = normalizeSuspenseChildren(vnode) ;(type as typeof SuspenseImpl).normalize(vnode)
vnode.ssContent = content
vnode.ssFallback = fallback
} }
if ( if (