diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts
index af332cd1e..5d40b4e5d 100644
--- a/packages/runtime-core/__tests__/hydration.spec.ts
+++ b/packages/runtime-core/__tests__/hydration.spec.ts
@@ -5,7 +5,9 @@ import {
nextTick,
VNode,
Portal,
- createStaticVNode
+ createStaticVNode,
+ Suspense,
+ onMounted
} from '@vue/runtime-dom'
import { renderToString } from '@vue/server-renderer'
import { mockWarn } from '@vue/shared'
@@ -28,6 +30,8 @@ const triggerEvent = (type: string, el: Element) => {
}
describe('SSR hydration', () => {
+ mockWarn()
+
test('text', async () => {
const msg = ref('foo')
const { vnode, container } = mountWithHydration('foo', () => msg.value)
@@ -94,7 +98,7 @@ describe('SSR hydration', () => {
expect(vnode.el.innerHTML).toBe(`bar`)
})
- test('fragment', async () => {
+ test('Fragment', async () => {
const msg = ref('foo')
const fn = jest.fn()
const { vnode, container } = mountWithHydration(
@@ -142,7 +146,7 @@ describe('SSR hydration', () => {
expect(vnode.el.innerHTML).toBe(`bar`)
})
- test('portal', async () => {
+ test('Portal', async () => {
const msg = ref('foo')
const fn = jest.fn()
const portalContainer = document.createElement('div')
@@ -271,9 +275,109 @@ describe('SSR hydration', () => {
expect(text.textContent).toBe('bye')
})
- describe('mismatch handling', () => {
- mockWarn()
+ test('Suspense', async () => {
+ const AsyncChild = {
+ async setup() {
+ const count = ref(0)
+ return () =>
+ h(
+ 'span',
+ {
+ onClick: () => {
+ count.value++
+ }
+ },
+ count.value
+ )
+ }
+ }
+ const { vnode, container } = mountWithHydration('0', () =>
+ h(Suspense, () => h(AsyncChild))
+ )
+ expect(vnode.el).toBe(container.firstChild)
+ // wait for hydration to finish
+ await new Promise(r => setTimeout(r))
+ triggerEvent('click', container.querySelector('span')!)
+ await nextTick()
+ expect(container.innerHTML).toBe(`1`)
+ })
+ test('Suspense (full integration)', async () => {
+ const mountedCalls: number[] = []
+ const asyncDeps: Promise[] = []
+
+ const AsyncChild = {
+ async setup(props: { n: number }) {
+ const count = ref(props.n)
+ onMounted(() => {
+ mountedCalls.push(props.n)
+ })
+ const p = new Promise(r => setTimeout(r, props.n * 10))
+ asyncDeps.push(p)
+ await p
+ return () =>
+ h(
+ 'span',
+ {
+ onClick: () => {
+ count.value++
+ }
+ },
+ count.value
+ )
+ }
+ }
+
+ const done = jest.fn()
+ const App = {
+ template: `
+
+
+
+ `,
+ components: {
+ AsyncChild
+ },
+ methods: {
+ done
+ }
+ }
+
+ const container = document.createElement('div')
+ // server render
+ container.innerHTML = await renderToString(h(App))
+ expect(container.innerHTML).toMatchInlineSnapshot(
+ `"12"`
+ )
+ // reset asyncDeps from ssr
+ asyncDeps.length = 0
+ // hydrate
+ createSSRApp(App).mount(container)
+
+ expect(mountedCalls.length).toBe(0)
+ expect(asyncDeps.length).toBe(2)
+
+ // wait for hydration to complete
+ await Promise.all(asyncDeps)
+ await new Promise(r => setTimeout(r))
+
+ // should flush buffered effects
+ expect(mountedCalls).toMatchObject([1, 2])
+ // should have removed fragment markers
+ expect(container.innerHTML).toMatch(`12`)
+
+ const span1 = container.querySelector('span')!
+ triggerEvent('click', span1)
+ await nextTick()
+ expect(container.innerHTML).toMatch(`22`)
+
+ const span2 = span1.nextSibling as Element
+ triggerEvent('click', span2)
+ await nextTick()
+ expect(container.innerHTML).toMatch(`23`)
+ })
+
+ describe('mismatch handling', () => {
test('text node', () => {
const { container } = mountWithHydration(`foo`, () => 'bar')
expect(container.textContent).toBe('bar')
diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts
index 5caa282b4..c0ee245ed 100644
--- a/packages/runtime-core/src/hydration.ts
+++ b/packages/runtime-core/src/hydration.ts
@@ -47,7 +47,6 @@ export function createHydrationFunctions(
const {
mt: mountComponent,
p: patch,
- n: next,
o: { patchProp, nextSibling, parentNode }
} = rendererInternals
@@ -152,16 +151,12 @@ export function createHydrationFunctions(
parentSuspense,
isSVGContainer(container)
)
- const subTree = vnode.component!.subTree
- if (subTree) {
- return next(subTree)
- } else {
- // no subTree means this is an async component
- // try to locate the ending node
- return isFragmentStart
- ? locateClosingAsyncAnchor(node)
- : nextSibling(node)
- }
+ // component may be async, so in the case of fragments we cannot rely
+ // on component's rendered output to determine the end of the fragment
+ // instead, we do a lookahead to find the end anchor node.
+ return isFragmentStart
+ ? locateClosingAsyncAnchor(node)
+ : nextSibling(node)
} else if (shapeFlag & ShapeFlags.PORTAL) {
if (domType !== DOMNodeTypes.COMMENT) {
return handleMismtach(node, vnode, parentComponent, parentSuspense)
diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts
index 775ca7cc9..df54210e3 100644
--- a/packages/runtime-core/src/vnode.ts
+++ b/packages/runtime-core/src/vnode.ts
@@ -212,7 +212,6 @@ export function createVNode(
): VNode {
if (!type) {
if (__DEV__) {
- debugger
warn(`fsef Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment