From c8d96837b871d7ad34cd73b4669338be5fdd59fd Mon Sep 17 00:00:00 2001 From: HcySunYang Date: Sat, 8 May 2021 06:42:58 +0800 Subject: [PATCH] fix(hydration): fix update before async component is hydrated (#3563) fix #3560 --- .../runtime-core/__tests__/hydration.spec.ts | 61 ++++++++++++++++++ packages/runtime-core/src/hydration.ts | 29 +++------ packages/runtime-core/src/renderer.ts | 62 ++++++++++++------- 3 files changed, 112 insertions(+), 40 deletions(-) diff --git a/packages/runtime-core/__tests__/hydration.spec.ts b/packages/runtime-core/__tests__/hydration.spec.ts index a1213145..8db566b5 100644 --- a/packages/runtime-core/__tests__/hydration.spec.ts +++ b/packages/runtime-core/__tests__/hydration.spec.ts @@ -626,6 +626,67 @@ describe('SSR hydration', () => { expect(spy).toHaveBeenCalled() }) + test('execute the updateComponent(AsyncComponentWrapper) before the async component is resolved', async () => { + const Comp = { + render() { + return h('h1', 'Async component') + } + } + let serverResolve: any + let AsyncComp = defineAsyncComponent( + () => + new Promise(r => { + serverResolve = r + }) + ) + + const bol = ref(true) + const App = { + setup() { + onMounted(() => { + // change state, this makes updateComponent(AsyncComp) execute before + // the async component is resolved + bol.value = false + }) + + return () => { + return [bol.value ? 'hello' : 'world', h(AsyncComp)] + } + } + } + + // server render + const htmlPromise = renderToString(h(App)) + serverResolve(Comp) + const html = await htmlPromise + expect(html).toMatchInlineSnapshot( + `"hello

Async component

"` + ) + + // hydration + let clientResolve: any + AsyncComp = defineAsyncComponent( + () => + new Promise(r => { + clientResolve = r + }) + ) + + const container = document.createElement('div') + container.innerHTML = html + createSSRApp(App).mount(container) + + // resolve + clientResolve(Comp) + await new Promise(r => setTimeout(r)) + + // should be hydrated now + expect(`Hydration node mismatch`).not.toHaveBeenWarned() + expect(container.innerHTML).toMatchInlineSnapshot( + `"world

Async component

"` + ) + }) + test('elements with camel-case in svg ', () => { const { vnode, container } = mountWithHydration( '', diff --git a/packages/runtime-core/src/hydration.ts b/packages/runtime-core/src/hydration.ts index 0085ce6e..45b6d7b0 100644 --- a/packages/runtime-core/src/hydration.ts +++ b/packages/runtime-core/src/hydration.ts @@ -8,7 +8,7 @@ import { VNodeHook } from './vnode' import { flushPostFlushCbs } from './scheduler' -import { ComponentOptions, ComponentInternalInstance } from './component' +import { ComponentInternalInstance } from './component' import { invokeDirectiveHook } from './directives' import { warn } from './warning' import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared' @@ -178,24 +178,15 @@ export function createHydrationFunctions( // on its sub-tree. vnode.slotScopeIds = slotScopeIds const container = parentNode(node)! - const hydrateComponent = () => { - mountComponent( - vnode, - container, - null, - parentComponent, - parentSuspense, - isSVGContainer(container), - optimized - ) - } - // async component - const loadAsync = (vnode.type as ComponentOptions).__asyncLoader - if (loadAsync) { - loadAsync().then(hydrateComponent) - } else { - hydrateComponent() - } + mountComponent( + vnode, + container, + null, + parentComponent, + parentSuspense, + isSVGContainer(container), + optimized + ) // 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. diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index c075b3e5..4e661bc9 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -16,6 +16,7 @@ import { } from './vnode' import { ComponentInternalInstance, + ComponentOptions, createComponentInstance, Data, setupComponent @@ -1430,31 +1431,50 @@ function baseCreateRenderer( instance.emit('hook:beforeMount') } - // render - if (__DEV__) { - startMeasure(instance, `render`) - } - const subTree = (instance.subTree = renderComponentRoot(instance)) - if (__DEV__) { - endMeasure(instance, `render`) - } - if (el && hydrateNode) { - if (__DEV__) { - startMeasure(instance, `hydrate`) - } // vnode has adopted host node - perform hydration instead of mount. - hydrateNode( - initialVNode.el as Node, - subTree, - instance, - parentSuspense, - null - ) - if (__DEV__) { - endMeasure(instance, `hydrate`) + const hydrateSubTree = () => { + if (__DEV__) { + startMeasure(instance, `render`) + } + instance.subTree = renderComponentRoot(instance) + if (__DEV__) { + endMeasure(instance, `render`) + } + if (__DEV__) { + startMeasure(instance, `hydrate`) + } + hydrateNode!( + el as Node, + instance.subTree, + instance, + parentSuspense, + null + ) + if (__DEV__) { + endMeasure(instance, `hydrate`) + } + } + + if (isAsyncWrapper(initialVNode)) { + (initialVNode.type as ComponentOptions).__asyncLoader!().then( + // note: we are moving the render call into an async callback, + // which means it won't track dependencies - but it's ok because + // a server-rendered async wrapper is already in resolved state + // and it will never need to change. + hydrateSubTree + ) + } else { + hydrateSubTree() } } else { + if (__DEV__) { + startMeasure(instance, `render`) + } + const subTree = (instance.subTree = renderComponentRoot(instance)) + if (__DEV__) { + endMeasure(instance, `render`) + } if (__DEV__) { startMeasure(instance, `patch`) }