|
|
@@ -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
|
|
|
}
|