container: Element
) => void
+const enum DOMNodeTypes {
+ ELEMENT = 1,
+ TEXT = 3,
+ COMMENT = 8
+}
+
+let hasHydrationMismatch = false
+
// 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.
// passed in via arguments.
export function createHydrationFunctions({
mt: mountComponent,
+ p: patch,
o: { patchProp, createText }
}: RendererInternals<Node, Element>) {
const hydrate: RootHydrateFunction = (vnode, container) => {
if (__DEV__ && !container.hasChildNodes()) {
- warn(`Attempting to hydrate existing markup but container is empty.`)
+ warn(
+ `Attempting to hydrate existing markup but container is empty. ` +
+ `Performing full mount instead.`
+ )
+ patch(null, vnode, container)
return
}
+ hasHydrationMismatch = false
hydrateNode(container.firstChild!, vnode)
flushPostFlushCbs()
+ if (hasHydrationMismatch) {
+ // this error should show up in production
+ console.error(`Hydration completed but contains mismatches.`)
+ }
}
- // TODO handle mismatches
const hydrateNode = (
node: Node,
vnode: VNode,
optimized = false
): Node | null => {
const { type, shapeFlag } = vnode
+ const domType = node.nodeType
+
vnode.el = node
+
switch (type) {
case Text:
+ if (domType !== DOMNodeTypes.TEXT) {
+ return handleMismtach(node, vnode, parentComponent)
+ }
+ if ((node as Text).data !== vnode.children) {
+ hasHydrationMismatch = true
+ __DEV__ &&
+ warn(
+ `Hydration text mismatch:` +
+ `\n- Client: ${JSON.stringify(vnode.children)}`,
+ `\n- Server: ${JSON.stringify((node as Text).data)}`
+ )
+ ;(node as Text).data = vnode.children as string
+ }
+ return node.nextSibling
case Comment:
+ if (domType !== DOMNodeTypes.COMMENT) {
+ return handleMismtach(node, vnode, parentComponent)
+ }
+ return node.nextSibling
case Static:
+ if (domType !== DOMNodeTypes.ELEMENT) {
+ return handleMismtach(node, vnode, parentComponent)
+ }
return node.nextSibling
case Fragment:
return hydrateFragment(node, vnode, parentComponent, optimized)
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
+ if (domType !== DOMNodeTypes.ELEMENT) {
+ return handleMismtach(node, vnode, parentComponent)
+ }
return hydrateElement(
node as Element,
vnode,
const subTree = vnode.component!.subTree
return (subTree.anchor || subTree.el).nextSibling
} else if (shapeFlag & ShapeFlags.PORTAL) {
- hydratePortal(vnode, parentComponent, optimized)
+ if (domType !== DOMNodeTypes.COMMENT) {
+ return handleMismtach(node, vnode, parentComponent)
+ }
+ hydratePortal(
+ vnode,
+ node.parentNode as Element,
+ parentComponent,
+ optimized
+ )
return node.nextSibling
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// TODO Suspense
parentComponent: ComponentInternalInstance | null,
optimized: boolean
) => {
- const { props, patchFlag } = vnode
+ const { props, patchFlag, shapeFlag } = vnode
// skip props & children if this is hoisted static nodes
if (patchFlag !== PatchFlags.HOISTED) {
// props
}
// children
if (
- vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
+ shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
// skip if element has innerHTML / textContent
!(props !== null && (props.innerHTML || props.textContent))
) {
- hydrateChildren(
+ let next = hydrateChildren(
el.firstChild,
vnode,
+ el,
parentComponent,
optimized || vnode.dynamicChildren !== null
)
+ while (next) {
+ hasHydrationMismatch = true
+ __DEV__ &&
+ warn(
+ `Hydration children mismatch: ` +
+ `server rendered element contains more child nodes than client vdom.`
+ )
+ // The SSRed DOM contains more nodes than it should. Remove them.
+ const cur = next
+ next = next.nextSibling
+ el.removeChild(cur)
+ }
+ } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
+ el.textContent = vnode.children as string
}
}
return el.nextSibling
const hydrateChildren = (
node: Node | null,
vnode: VNode,
+ container: Element,
parentComponent: ComponentInternalInstance | null,
optimized: boolean
): Node | null => {
const children = vnode.children as VNode[]
optimized = optimized || vnode.dynamicChildren !== null
- for (let i = 0; node != null && i < children.length; i++) {
+ for (let i = 0; i < children.length; i++) {
const vnode = optimized
? children[i]
: (children[i] = normalizeVNode(children[i]))
- node = hydrateNode(node, vnode, parentComponent, optimized)
+ if (node) {
+ node = hydrateNode(node, vnode, parentComponent, optimized)
+ } else {
+ hasHydrationMismatch = true
+ __DEV__ &&
+ warn(
+ `Hydration children mismatch: ` +
+ `server rendered element contains fewer child nodes than client vdom.`
+ )
+ // the SSRed DOM didn't contain enough nodes. Mount the missing ones.
+ patch(null, vnode, container)
+ }
}
return node
}
parentComponent: ComponentInternalInstance | null,
optimized: boolean
) => {
- const parent = node.parentNode!
+ const parent = node.parentNode as Element
parent.insertBefore((vnode.el = createText('')), node)
- const next = hydrateChildren(node, vnode, parentComponent, optimized)
+ const next = hydrateChildren(
+ node,
+ vnode,
+ parent,
+ parentComponent,
+ optimized
+ )
parent.insertBefore((vnode.anchor = createText('')), next)
return next
}
const hydratePortal = (
vnode: VNode,
+ container: Element,
parentComponent: ComponentInternalInstance | null,
optimized: boolean
) => {
? document.querySelector(targetSelector)
: targetSelector)
if (target != null && vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
- hydrateChildren(target.firstChild, vnode, parentComponent, optimized)
+ hydrateChildren(
+ target.firstChild,
+ vnode,
+ container,
+ parentComponent,
+ optimized
+ )
}
}
+ const handleMismtach = (
+ node: Node,
+ vnode: VNode,
+ parentComponent: ComponentInternalInstance | null
+ ) => {
+ hasHydrationMismatch = true
+ __DEV__ &&
+ warn(
+ `Hydration node mismatch:\n- Client vnode:`,
+ vnode.type,
+ `\n- Server rendered DOM:`,
+ node
+ )
+ vnode.el = null
+ const next = node.nextSibling
+ const container = node.parentNode as Element
+ container.removeChild(node)
+ // TODO Suspense and SVG
+ patch(null, vnode, container, next, parentComponent)
+ return next
+ }
+
return [hydrate, hydrateNode] as const
}