From 59cfe638c617060312773ba2fe57d7be8dcf2d9f Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Thu, 15 Dec 2022 01:24:08 +0800 Subject: [PATCH] fix(runtime-core): Fix hydration crash when deep nested async component is not hydrated yet --- .../runtime-core/__tests__/hydration.spec.ts | 92 +++++++++++++++++++ packages/runtime-core/src/renderer.ts | 64 ++++++++----- 2 files changed, 132 insertions(+), 24 deletions(-) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index ae152ba694f..a1e1886ec39 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -913,6 +913,98 @@ describe('SSR hydration', () => { `"

Async component

"` ) }) + + test('hydrate safely when property used by deep nested async setup changed before render', async () => { + // let serverResolve: any + + const AsyncComp = { + async setup() { + await new Promise(r => r()) + return () => { + return h('h1', 'Async component') + } + } + } + + const AsyncWrapper = { + render() { + return h(AsyncComp) + } + } + const AsyncWrapperWrapper = { + render() { + return h(AsyncWrapper) + } + } + + const SiblingComp = { + setup() { + return () => { + bol.value = false + return h('span') + } + } + } + + const bol = ref(true) + + const App = { + setup() { + return () => { + return [ + h( + Suspense, + {}, + { + default: () => { + return [ + h('main', {}, [ + h(AsyncWrapperWrapper, { + prop: bol.value ? 'hello' : 'world' + }), + h(SiblingComp) + ]) + ] + } + } + ) + ] + } + } + } + + // server render + + const html = await renderToString(h(App)) + + expect(html).toMatchInlineSnapshot( + `"

Async component

"` + ) + + expect(bol.value).toBe(false) + + // hydration + + // reset the value + bol.value = true + expect(bol.value).toBe(true) + + const container = document.createElement('div') + container.innerHTML = html + createSSRApp(App).mount(container) + + await new Promise(resolve => { + setTimeout(resolve, 0) + }) + + expect(bol.value).toBe(false) + + // should be hydrated now + // expect(`Hydration node mismatch`).toHaveBeenWarned() + expect(container.innerHTML).toMatchInlineSnapshot( + `"

Async component

"` + ) + }) // #3787 test('unmount async wrapper before load', async () => { let resolve: any diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index d46cb5e915e..c26f585209c 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1436,32 +1436,48 @@ function baseCreateRenderer( // #2458: deference mount-only object parameters to prevent memleaks initialVNode = container = anchor = null as any } else { - // we are trying to update some async comp before hydration - // this will cause crash because we don't know the root node yet - if ( - __FEATURE_SUSPENSE__ && - instance.subTree.shapeFlag & ShapeFlags.COMPONENT && - // this happens only during hydration - instance.subTree.component?.subTree == null && - // we don't know the subTree yet because we haven't resolve it - instance.subTree.component?.asyncResolved === false - ) { - // only sync the properties and abort the rest of operations - let { next, vnode } = instance - toggleRecurse(instance, false) - if (next) { - next.el = vnode.el - updateComponentPreRender(instance, next, optimized) + if (__FEATURE_SUSPENSE__) { + const locateNonHydratedAsyncRoot = ( + instance: ComponentInternalInstance + ): ComponentInternalInstance | null => { + if (instance.subTree.shapeFlag & ShapeFlags.COMPONENT) { + if ( + // this happens only during hydration + instance.subTree.component?.subTree == null && + // we don't know the subTree yet because we haven't resolve it + instance.subTree.component?.asyncResolved === false + ) { + return instance.subTree.component! + } else { + return locateNonHydratedAsyncRoot(instance.subTree.component!) + } + } else { + return null + } } - toggleRecurse(instance, true) - // and continue the rest of operations once the deps are resolved - instance.subTree.component?.asyncDep?.then(() => { - // the instance may be destroyed during the time period - if (!instance.isUnmounted) { - componentUpdateFn() + + const nonHydratedAsyncRoot = locateNonHydratedAsyncRoot(instance) + + // we are trying to update some async comp before hydration + // this will cause crash because we don't know the root node yet + if (nonHydratedAsyncRoot != null) { + // only sync the properties and abort the rest of operations + let { next, vnode } = instance + toggleRecurse(instance, false) + if (next) { + next.el = vnode.el + updateComponentPreRender(instance, next, optimized) } - }) - return + toggleRecurse(instance, true) + // and continue the rest of operations once the deps are resolved + nonHydratedAsyncRoot.asyncDep?.then(() => { + // the instance may be destroyed during the time period + if (!instance.isUnmounted) { + componentUpdateFn() + } + }) + return + } } // updateComponent