From bd3f6f7e1b82f50eabd685d02a453439d7fe4bbc Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Mon, 5 Dec 2022 18:08:15 +0800 Subject: [PATCH 01/13] fix(runtime-core): Try to fix suspense error --- .../runtime-core/__tests__/hydration.spec.ts | 84 +++++++++++++++++++ .../runtime-core/src/components/Suspense.ts | 30 ++++--- packages/runtime-core/src/renderer.ts | 23 +++++ 3 files changed, 125 insertions(+), 12 deletions(-) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index a5f056f385c..87220863495 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -859,6 +859,90 @@ describe('SSR hydration', () => { ) }) + test('hydrate safely when property used by 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 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(AsyncWrapper, { 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/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 85f58c58916..15beb613aa9 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -226,18 +226,24 @@ function patchSuspense( if (suspense.deps <= 0) { suspense.resolve() } else if (isInFallback) { - patch( - activeBranch, - newFallback, - container, - anchor, - parentComponent, - null, // fallback tree will not have suspense context - namespace, - slotScopeIds, - optimized - ) - setActiveBranch(suspense, newFallback) + // It's possible that the app is in hydrating state when patching the suspense instance + // if someone update the dependency during component setup in children of suspense boundary + // And that would be problemtic because we aren't actually showing a fallback content when patchSuspense is called. + // In such case, patch of fallback content should be no op + if (!isHydrating) { + patch( + activeBranch, + newFallback, + container, + anchor, + parentComponent, + null, // fallback tree will not have suspense context + namespace, + slotScopeIds, + optimized + ) + setActiveBranch(suspense, newFallback) + } } } else { // toggled before pending tree is resolved diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 8e65b4271c4..e45f392f89c 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1447,6 +1447,29 @@ 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 + if (next) { + next.el = vnode.el + updateComponentPreRender(instance, next, optimized) + } + // and continue the rest of operations once the deps are resolved + instance.subTree.component?.asyncDep?.then(() => { + componentUpdateFn() + }) + return + } + // updateComponent // This is triggered by mutation of component's own state (next: null) // OR parent calling processComponent (next: VNode) From 07d8b5471b927f03df1763cc8c2db238a73d9a41 Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Wed, 7 Dec 2022 19:06:41 +0800 Subject: [PATCH 02/13] fix(runtime-core): Ensure it won't patch the dom if component is already unmounted --- packages/runtime-core/src/renderer.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index e45f392f89c..5335c5216bc 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1459,13 +1459,18 @@ function baseCreateRenderer( ) { // 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) } + toggleRecurse(instance, true) // and continue the rest of operations once the deps are resolved instance.subTree.component?.asyncDep?.then(() => { - componentUpdateFn() + // the instance may be destroyed during the time period + if (!instance.isUnmounted) { + componentUpdateFn() + } }) return } From 0a19f3d30833bb47537455efc67f464826b016e5 Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Thu, 15 Dec 2022 01:24:08 +0800 Subject: [PATCH 03/13] 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 87220863495..f5c4decc608 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -943,6 +943,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 5335c5216bc..cec62949ac5 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1447,32 +1447,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 From bdf60f2d9e1c7fcd35eb47d0b3e6742adddc0b8a Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Tue, 24 Oct 2023 22:57:51 +0800 Subject: [PATCH 04/13] fix(runtime-core): Check asyncDep instead of subTree The null el can also happen during client rendering --- packages/runtime-core/src/renderer.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index cec62949ac5..538c8742b36 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1453,9 +1453,8 @@ function baseCreateRenderer( ): 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 + // this happens during hydration or updating a component that resolve to a unresolved async component + instance.subTree.component?.asyncDep != null && instance.subTree.component?.asyncResolved === false ) { return instance.subTree.component! From f10e67ad5b0b1e0ce9a75e0269ffa6ee5fe6c8f6 Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Tue, 24 Oct 2023 23:13:38 +0800 Subject: [PATCH 05/13] fix(runtime-core): Fix test snapshot error caused by util change --- packages/runtime-core/__tests__/hydration.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index f5c4decc608..6b8b65c1ba9 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -916,7 +916,7 @@ describe('SSR hydration', () => { const html = await renderToString(h(App)) expect(html).toMatchInlineSnapshot( - `"

Async component

"` + `"

Async component

"` ) expect(bol.value).toBe(false) @@ -940,7 +940,7 @@ describe('SSR hydration', () => { // should be hydrated now // expect(`Hydration node mismatch`).toHaveBeenWarned() expect(container.innerHTML).toMatchInlineSnapshot( - `"

Async component

"` + `"

Async component

"` ) }) @@ -1008,7 +1008,7 @@ describe('SSR hydration', () => { const html = await renderToString(h(App)) expect(html).toMatchInlineSnapshot( - `"

Async component

"` + `"

Async component

"` ) expect(bol.value).toBe(false) @@ -1032,7 +1032,7 @@ describe('SSR hydration', () => { // should be hydrated now // expect(`Hydration node mismatch`).toHaveBeenWarned() expect(container.innerHTML).toMatchInlineSnapshot( - `"

Async component

"` + `"

Async component

"` ) }) // #3787 From e5910c774fb77df58e9f0f44812124fb57d09aaf Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Sat, 28 Oct 2023 02:42:42 +0800 Subject: [PATCH 06/13] fix(runtime-core): Fix client side suspense update crash in proper way The initialVNode of async setup missed the el property, which in turns cause its parent component to crash during update --- packages/runtime-core/src/renderer.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 538c8742b36..2c6f3fc3d56 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1241,6 +1241,7 @@ function baseCreateRenderer( if (!initialVNode.el) { const placeholder = (instance.subTree = createVNode(Comment)) processCommentNode(null, placeholder, container!, anchor) + initialVNode.el = placeholder.el } return } @@ -1453,8 +1454,9 @@ function baseCreateRenderer( ): ComponentInternalInstance | null => { if (instance.subTree.shapeFlag & ShapeFlags.COMPONENT) { if ( - // this happens during hydration or updating a component that resolve to a unresolved async component - instance.subTree.component?.asyncDep != null && + // 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! From 8adef84394713631d4af396ce4d824bf96c2cf2d Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Thu, 2 Nov 2023 20:50:49 +0800 Subject: [PATCH 07/13] fix(runtime-core): Add test for suspense crash caused by update before resolve --- .../__tests__/components/Suspense.spec.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index 6fec755106a..d647a96ecdd 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -537,6 +537,51 @@ describe('Suspense', () => { expect(unmounted).not.toHaveBeenCalled() }) + // vuetifyjs/vuetify#15207 + test('update prop of async element before suspense resolve', async () => { + let resolve: () => void + const mounted = new Promise(r => { + resolve = r + }) + const Async = { + async setup() { + onMounted(() => { + resolve() + }) + const p = new Promise(r => setTimeout(r, 1)) + await p + return () => h('div', 'async') + } + } + + const Comp: ComponentOptions<{ data: string }> = { + props: ['data'], + setup(props) { + return () => h(Async, { 'data-test': props.data }) + } + } + + const Root = { + setup() { + const data = ref('1') + onMounted(() => { + data.value = '2' + }) + return () => + h(Suspense, null, { + default: h(Comp, { data: data.value }), + fallback: h('div', 'fallback') + }) + } + } + + const root = nodeOps.createElement('div') + render(h(Root), root) + expect(serializeInner(root)).toBe(`
fallback
`) + await mounted + expect(serializeInner(root)).toBe(`
async
`) + }) + test('nested suspense (parent resolves first)', async () => { const calls: string[] = [] From 4e8e4f64ebc694ccf75beb78314a65e26b8dec0b Mon Sep 17 00:00:00 2001 From: mmis1000 Date: Sat, 4 Nov 2023 01:05:53 +0800 Subject: [PATCH 08/13] chore(runtime-core): explain the async element initialVNode.el patching --- packages/runtime-core/src/renderer.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 2c6f3fc3d56..df799f8b0e2 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1241,6 +1241,9 @@ function baseCreateRenderer( if (!initialVNode.el) { const placeholder = (instance.subTree = createVNode(Comment)) processCommentNode(null, placeholder, container!, anchor) + // This noramlly gets setup by the following `setupRenderEffect`. + // But the call is skipped in initial mounting of async element. + // Thus, manually patching is required here or it will result in a crash during parent component update. initialVNode.el = placeholder.el } return From b338587185d1943dff3a7c60da89d24edc5dc076 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Dec 2023 18:04:49 +0800 Subject: [PATCH 09/13] chore: update snapshots --- packages/runtime-core/__tests__/hydration.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index 6b8b65c1ba9..f5c4decc608 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -916,7 +916,7 @@ describe('SSR hydration', () => { const html = await renderToString(h(App)) expect(html).toMatchInlineSnapshot( - `"

Async component

"` + `"

Async component

"` ) expect(bol.value).toBe(false) @@ -940,7 +940,7 @@ describe('SSR hydration', () => { // should be hydrated now // expect(`Hydration node mismatch`).toHaveBeenWarned() expect(container.innerHTML).toMatchInlineSnapshot( - `"

Async component

"` + `"

Async component

"` ) }) @@ -1008,7 +1008,7 @@ describe('SSR hydration', () => { const html = await renderToString(h(App)) expect(html).toMatchInlineSnapshot( - `"

Async component

"` + `"

Async component

"` ) expect(bol.value).toBe(false) @@ -1032,7 +1032,7 @@ describe('SSR hydration', () => { // should be hydrated now // expect(`Hydration node mismatch`).toHaveBeenWarned() expect(container.innerHTML).toMatchInlineSnapshot( - `"

Async component

"` + `"

Async component

"` ) }) // #3787 From a1b4140e41474016b8becb16c4e93b0fcf6a5a59 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Dec 2023 19:06:53 +0800 Subject: [PATCH 10/13] chore: cleanup tests --- .../runtime-core/__tests__/hydration.spec.ts | 143 +++++++----------- 1 file changed, 56 insertions(+), 87 deletions(-) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index f5c4decc608..d43e7b2dfc9 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -812,17 +812,17 @@ describe('SSR hydration', () => { }) ) - const bol = ref(true) + const toggle = ref(true) const App = { setup() { onMounted(() => { // change state, this makes updateComponent(AsyncComp) execute before // the async component is resolved - bol.value = false + toggle.value = false }) return () => { - return [bol.value ? 'hello' : 'world', h(AsyncComp)] + return [toggle.value ? 'hello' : 'world', h(AsyncComp)] } } } @@ -860,14 +860,12 @@ describe('SSR hydration', () => { }) test('hydrate safely when property used by async setup changed before render', async () => { - // let serverResolve: any + const toggle = ref(true) const AsyncComp = { async setup() { await new Promise(r => r()) - return () => { - return h('h1', 'Async component') - } + return () => h('h1', 'Async component') } } @@ -879,53 +877,45 @@ describe('SSR hydration', () => { const SiblingComp = { setup() { - return () => { - bol.value = false - return h('span') - } + toggle.value = false + return () => h('span') } } - const bol = ref(true) - const App = { setup() { - return () => { - return [ - h( - Suspense, - {}, - { - default: () => { - return [ - h('main', {}, [ - h(AsyncWrapper, { prop: bol.value ? 'hello' : 'world' }), - h(SiblingComp) - ]) - ] - } - } - ) - ] - } + return () => + h( + Suspense, + {}, + { + default: () => [ + h('main', {}, [ + h(AsyncWrapper, { + prop: toggle.value ? 'hello' : 'world' + }), + h(SiblingComp) + ]) + ] + } + ) } } // server render - const html = await renderToString(h(App)) expect(html).toMatchInlineSnapshot( - `"

Async component

"` + `"

Async component

"` ) - expect(bol.value).toBe(false) + expect(toggle.value).toBe(false) // hydration // reset the value - bol.value = true - expect(bol.value).toBe(true) + toggle.value = true + expect(toggle.value).toBe(true) const container = document.createElement('div') container.innerHTML = html @@ -935,89 +925,68 @@ describe('SSR hydration', () => { setTimeout(resolve, 0) }) - expect(bol.value).toBe(false) + expect(toggle.value).toBe(false) // should be hydrated now - // expect(`Hydration node mismatch`).toHaveBeenWarned() expect(container.innerHTML).toMatchInlineSnapshot( - `"

Async component

"` + `"

Async component

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

Async component

"` + `"

Async component

"` ) - expect(bol.value).toBe(false) + expect(toggle.value).toBe(false) // hydration // reset the value - bol.value = true - expect(bol.value).toBe(true) + toggle.value = true + expect(toggle.value).toBe(true) const container = document.createElement('div') container.innerHTML = html @@ -1027,14 +996,14 @@ describe('SSR hydration', () => { setTimeout(resolve, 0) }) - expect(bol.value).toBe(false) + expect(toggle.value).toBe(false) // should be hydrated now - // expect(`Hydration node mismatch`).toHaveBeenWarned() expect(container.innerHTML).toMatchInlineSnapshot( - `"

Async component

"` + `"

Async component

"` ) }) + // #3787 test('unmount async wrapper before load', async () => { let resolve: any From a61280c391fc644723158e470418077ce2a8c943 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Dec 2023 19:09:03 +0800 Subject: [PATCH 11/13] chore: cleanup comments --- packages/runtime-core/src/components/Suspense.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index 15beb613aa9..469dd87cbd9 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -226,10 +226,12 @@ function patchSuspense( if (suspense.deps <= 0) { suspense.resolve() } else if (isInFallback) { - // It's possible that the app is in hydrating state when patching the suspense instance - // if someone update the dependency during component setup in children of suspense boundary - // And that would be problemtic because we aren't actually showing a fallback content when patchSuspense is called. - // In such case, patch of fallback content should be no op + // It's possible that the app is in hydrating state when patching the + // suspense instance. If someone updates the dependency during component + // setup in children of suspense boundary, that would be problemtic + // because we aren't actually showing a fallback content when + // patchSuspense is called. In such case, patch of fallback content + // should be no op if (!isHydrating) { patch( activeBranch, From d720dbfa777a4443aba23ad147bc838a638a35a8 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Dec 2023 19:26:52 +0800 Subject: [PATCH 12/13] chore: refactor implementation --- packages/runtime-core/src/renderer.ts | 41 +++++++++++---------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index df799f8b0e2..04110726719 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1451,33 +1451,14 @@ function baseCreateRenderer( // #2458: deference mount-only object parameters to prevent memleaks initialVNode = container = anchor = null as any } else { - 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 - } - } + let { next, bu, u, parent, vnode } = instance + if (__FEATURE_SUSPENSE__) { 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) { + if (nonHydratedAsyncRoot) { // only sync the properties and abort the rest of operations - let { next, vnode } = instance toggleRecurse(instance, false) if (next) { next.el = vnode.el @@ -1485,7 +1466,7 @@ function baseCreateRenderer( } toggleRecurse(instance, true) // and continue the rest of operations once the deps are resolved - nonHydratedAsyncRoot.asyncDep?.then(() => { + nonHydratedAsyncRoot.asyncDep!.then(() => { // the instance may be destroyed during the time period if (!instance.isUnmounted) { componentUpdateFn() @@ -1498,7 +1479,6 @@ function baseCreateRenderer( // updateComponent // This is triggered by mutation of component's own state (next: null) // OR parent calling processComponent (next: VNode) - let { next, bu, u, parent, vnode } = instance let originNext = next let vnodeHook: VNodeHook | null | undefined if (__DEV__) { @@ -2537,3 +2517,16 @@ function getSequence(arr: number[]): number[] { } return result } + +function locateNonHydratedAsyncRoot( + instance: ComponentInternalInstance +): ComponentInternalInstance | undefined { + const subComponent = instance.subTree.component + if (subComponent) { + if (subComponent.asyncDep && !subComponent.asyncResolved) { + return subComponent + } else { + return locateNonHydratedAsyncRoot(subComponent) + } + } +} From fbcdc72e6c7ed0e4883b3e88a8cee4de11e6c84c Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 12 Dec 2023 19:33:36 +0800 Subject: [PATCH 13/13] chore: add promise timeout --- packages/runtime-core/__tests__/hydration.spec.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index d43e7b2dfc9..0805d5e3f13 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -864,7 +864,7 @@ describe('SSR hydration', () => { const AsyncComp = { async setup() { - await new Promise(r => r()) + await new Promise(r => setTimeout(r, 10)) return () => h('h1', 'Async component') } } @@ -921,9 +921,7 @@ describe('SSR hydration', () => { container.innerHTML = html createSSRApp(App).mount(container) - await new Promise(resolve => { - setTimeout(resolve, 0) - }) + await new Promise(r => setTimeout(r, 10)) expect(toggle.value).toBe(false) @@ -938,7 +936,7 @@ describe('SSR hydration', () => { const AsyncComp = { async setup() { - await new Promise(r => r()) + await new Promise(r => setTimeout(r, 10)) return () => h('h1', 'Async component') } } @@ -992,9 +990,7 @@ describe('SSR hydration', () => { container.innerHTML = html createSSRApp(App).mount(container) - await new Promise(resolve => { - setTimeout(resolve, 0) - }) + await new Promise(r => setTimeout(r, 10)) expect(toggle.value).toBe(false)