feat(ssr/suspense): suspense hydration

In order to support hydration of async components, server-rendered
fragments must be explicitly marked with comment nodes.
This commit is contained in:
Evan You
2020-03-12 22:19:41 -04:00
parent b3d7d64931
commit a3cc970030
19 changed files with 385 additions and 139 deletions

View File

@@ -1,5 +1,5 @@
import { VNode, normalizeVNode, Text, Comment, Static, Fragment } from './vnode'
import { queuePostFlushCb, flushPostFlushCbs } from './scheduler'
import { flushPostFlushCbs } from './scheduler'
import { ComponentInternalInstance } from './component'
import { invokeDirectiveHook } from './directives'
import { warn } from './warning'
@@ -11,6 +11,11 @@ import {
isString
} from '@vue/shared'
import { RendererInternals } from './renderer'
import {
SuspenseImpl,
SuspenseBoundary,
queueEffectWithSuspense
} from './components/Suspense'
export type RootHydrateFunction = (
vnode: VNode<Node, Element>,
@@ -25,16 +30,27 @@ const enum DOMNodeTypes {
let hasMismatch = false
const isSVGContainer = (container: Element) =>
/svg/.test(container.namespaceURI!) && container.tagName !== 'foreignObject'
const isComment = (node: Node): node is Comment =>
node.nodeType === DOMNodeTypes.COMMENT
// Note: hydration is DOM-specific
// But we have to place it in core due to tight coupling with core - splitting
// it out creates a ton of unnecessary complexity.
// Hydration also depends on some renderer internal logic which needs to be
// passed in via arguments.
export function createHydrationFunctions({
mt: mountComponent,
p: patch,
o: { patchProp, createText }
}: RendererInternals<Node, Element>) {
export function createHydrationFunctions(
rendererInternals: RendererInternals<Node, Element>
) {
const {
mt: mountComponent,
p: patch,
n: next,
o: { patchProp, nextSibling, parentNode }
} = rendererInternals
const hydrate: RootHydrateFunction = (vnode, container) => {
if (__DEV__ && !container.hasChildNodes()) {
warn(
@@ -45,7 +61,7 @@ export function createHydrationFunctions({
return
}
hasMismatch = false
hydrateNode(container.firstChild!, vnode)
hydrateNode(container.firstChild!, vnode, null, null)
flushPostFlushCbs()
if (hasMismatch && !__TEST__) {
// this error should show up in production
@@ -56,7 +72,8 @@ export function createHydrationFunctions({
const hydrateNode = (
node: Node,
vnode: VNode,
parentComponent: ComponentInternalInstance | null = null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized = false
): Node | null => {
const { type, shapeFlag } = vnode
@@ -67,7 +84,7 @@ export function createHydrationFunctions({
switch (type) {
case Text:
if (domType !== DOMNodeTypes.TEXT) {
return handleMismtach(node, vnode, parentComponent)
return handleMismtach(node, vnode, parentComponent, parentSuspense)
}
if ((node as Text).data !== vnode.children) {
hasMismatch = true
@@ -79,48 +96,83 @@ export function createHydrationFunctions({
)
;(node as Text).data = vnode.children as string
}
return node.nextSibling
return nextSibling(node)
case Comment:
if (domType !== DOMNodeTypes.COMMENT) {
return handleMismtach(node, vnode, parentComponent)
return handleMismtach(node, vnode, parentComponent, parentSuspense)
}
return node.nextSibling
return nextSibling(node)
case Static:
if (domType !== DOMNodeTypes.ELEMENT) {
return handleMismtach(node, vnode, parentComponent)
return handleMismtach(node, vnode, parentComponent, parentSuspense)
}
return node.nextSibling
return nextSibling(node)
case Fragment:
return hydrateFragment(node, vnode, parentComponent, optimized)
if (domType !== DOMNodeTypes.COMMENT) {
return handleMismtach(node, vnode, parentComponent, parentSuspense)
}
return hydrateFragment(
node as Comment,
vnode,
parentComponent,
parentSuspense,
optimized
)
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
if (
domType !== DOMNodeTypes.ELEMENT ||
vnode.type !== (node as Element).tagName.toLowerCase()
) {
return handleMismtach(node, vnode, parentComponent)
return handleMismtach(node, vnode, parentComponent, parentSuspense)
}
return hydrateElement(
node as Element,
vnode,
parentComponent,
parentSuspense,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// when setting up the render effect, if the initial vnode already
// has .el set, the component will perform hydration instead of mount
// on its sub-tree.
mountComponent(vnode, null, null, parentComponent, null, false)
const container = parentNode(node)!
mountComponent(
vnode,
container,
null,
parentComponent,
parentSuspense,
isSVGContainer(container)
)
const subTree = vnode.component!.subTree
return (subTree.anchor || subTree.el).nextSibling
if (subTree) {
return next(subTree)
} else {
// no subTree means this is an async component
// try to locate the ending node
return isComment(node) && node.data === '1'
? locateClosingAsyncAnchor(node)
: nextSibling(node)
}
} else if (shapeFlag & ShapeFlags.PORTAL) {
if (domType !== DOMNodeTypes.COMMENT) {
return handleMismtach(node, vnode, parentComponent)
return handleMismtach(node, vnode, parentComponent, parentSuspense)
}
hydratePortal(vnode, parentComponent, optimized)
return node.nextSibling
hydratePortal(vnode, parentComponent, parentSuspense, optimized)
return nextSibling(node)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// TODO Suspense
return (vnode.type as typeof SuspenseImpl).hydrate(
node,
vnode,
parentComponent,
parentSuspense,
isSVGContainer(parentNode(node)!),
optimized,
rendererInternals,
hydrateNode
)
} else if (__DEV__) {
warn('Invalid HostVNode type:', type, `(${typeof type})`)
}
@@ -132,6 +184,7 @@ export function createHydrationFunctions({
el: Element,
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized: boolean
) => {
optimized = optimized || vnode.dynamicChildren !== null
@@ -161,9 +214,9 @@ export function createHydrationFunctions({
invokeDirectiveHook(onVnodeBeforeMount, parentComponent, vnode)
}
if (onVnodeMounted != null) {
queuePostFlushCb(() => {
queueEffectWithSuspense(() => {
invokeDirectiveHook(onVnodeMounted, parentComponent, vnode)
})
}, parentSuspense)
}
}
// children
@@ -177,6 +230,7 @@ export function createHydrationFunctions({
vnode,
el,
parentComponent,
parentSuspense,
optimized
)
let hasWarned = false
@@ -215,6 +269,7 @@ export function createHydrationFunctions({
vnode: VNode,
container: Element,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized: boolean
): Node | null => {
optimized = optimized || vnode.dynamicChildren !== null
@@ -226,7 +281,13 @@ export function createHydrationFunctions({
? children[i]
: (children[i] = normalizeVNode(children[i]))
if (node) {
node = hydrateNode(node, vnode, parentComponent, optimized)
node = hydrateNode(
node,
vnode,
parentComponent,
parentSuspense,
optimized
)
} else {
hasMismatch = true
if (__DEV__ && !hasWarned) {
@@ -237,34 +298,43 @@ export function createHydrationFunctions({
hasWarned = true
}
// the SSRed DOM didn't contain enough nodes. Mount the missing ones.
patch(null, vnode, container)
patch(
null,
vnode,
container,
null,
parentComponent,
parentSuspense,
isSVGContainer(container)
)
}
}
return node
}
const hydrateFragment = (
node: Node,
node: Comment,
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized: boolean
) => {
const parent = node.parentNode as Element
parent.insertBefore((vnode.el = createText('')), node)
const next = hydrateChildren(
node,
vnode,
parent,
parentComponent,
optimized
return nextSibling(
(vnode.anchor = hydrateChildren(
nextSibling(node)!,
vnode,
parentNode(node)!,
parentComponent,
parentSuspense,
optimized
)!)
)
parent.insertBefore((vnode.anchor = createText('')), next)
return next
}
const hydratePortal = (
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized: boolean
) => {
const targetSelector = vnode.props && vnode.props.target
@@ -277,6 +347,7 @@ export function createHydrationFunctions({
vnode,
target,
parentComponent,
parentSuspense,
optimized
)
} else if (__DEV__) {
@@ -290,7 +361,8 @@ export function createHydrationFunctions({
const handleMismtach = (
node: Node,
vnode: VNode,
parentComponent: ComponentInternalInstance | null
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null
) => {
hasMismatch = true
__DEV__ &&
@@ -298,16 +370,43 @@ export function createHydrationFunctions({
`Hydration node mismatch:\n- Client vnode:`,
vnode.type,
`\n- Server rendered DOM:`,
node
node,
node.nodeType === DOMNodeTypes.TEXT ? `(text)` : ``
)
vnode.el = null
const next = node.nextSibling
const container = node.parentNode as Element
const next = nextSibling(node)
const container = parentNode(node)!
container.removeChild(node)
// TODO Suspense and SVG
patch(null, vnode, container, next, parentComponent)
// TODO Suspense
patch(
null,
vnode,
container,
next,
parentComponent,
parentSuspense,
isSVGContainer(container)
)
return next
}
const locateClosingAsyncAnchor = (node: Node | null): Node | null => {
let match = 0
while (node) {
node = nextSibling(node)
if (node && isComment(node)) {
if (node.data === '1') match++
if (node.data === '0') {
if (match === 0) {
return nextSibling(node)
} else {
match--
}
}
}
}
return node
}
return [hydrate, hydrateNode] as const
}