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