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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user