Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(hydration): refactor async component hydration #3563

Merged
merged 3 commits into from
May 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions packages/runtime-core/__tests__/hydration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<h1>Async component</h1><!--]-->"`
)

// 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<h1>Async component</h1><!--]-->"`
)
})

test('elements with camel-case in svg ', () => {
const { vnode, container } = mountWithHydration(
'<animateTransform></animateTransform>',
Expand Down
29 changes: 10 additions & 19 deletions packages/runtime-core/src/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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.
Expand Down
60 changes: 40 additions & 20 deletions packages/runtime-core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from './vnode'
import {
ComponentInternalInstance,
ComponentOptions,
createComponentInstance,
Data,
setupComponent
Expand Down Expand Up @@ -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) {
// vnode has adopted host node - perform hydration instead of mount.
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, `hydrate`)
startMeasure(instance, `render`)
}
// vnode has adopted host node - perform hydration instead of mount.
hydrateNode(
initialVNode.el as Node,
subTree,
instance,
parentSuspense,
null
)
const subTree = (instance.subTree = renderComponentRoot(instance))
if (__DEV__) {
endMeasure(instance, `hydrate`)
endMeasure(instance, `render`)
}
} else {
if (__DEV__) {
startMeasure(instance, `patch`)
}
Expand Down