import { VNode, normalizeVNode, Text, Comment, Static, Fragment, VNodeHook, createVNode, createTextVNode } from './vnode' import { flushPostFlushCbs } from './scheduler' import { ComponentInternalInstance } from './component' import { invokeDirectiveHook } from './directives' import { warn } from './warning' import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared' import { RendererInternals, invokeVNodeHook, setRef } from './renderer' import { SuspenseImpl, SuspenseBoundary, queueEffectWithSuspense } from './components/Suspense' import { TeleportImpl, TeleportVNode } from './components/Teleport' import { isAsyncWrapper } from './apiAsyncComponent' export type RootHydrateFunction = ( vnode: VNode, container: Element | ShadowRoot ) => void const enum DOMNodeTypes { ELEMENT = 1, TEXT = 3, COMMENT = 8 } 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( rendererInternals: RendererInternals ) { const { mt: mountComponent, p: patch, o: { patchProp, nextSibling, parentNode, remove, insert, createComment } } = rendererInternals const hydrate: RootHydrateFunction = (vnode, container) => { if (!container.hasChildNodes()) { __DEV__ && warn( `Attempting to hydrate existing markup but container is empty. ` + `Performing full mount instead.` ) patch(null, vnode, container) flushPostFlushCbs() return } hasMismatch = false hydrateNode(container.firstChild!, vnode, null, null, null) flushPostFlushCbs() if (hasMismatch && !__TEST__) { // this error should show up in production console.error(`Hydration completed but contains mismatches.`) } } const hydrateNode = ( node: Node, vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, slotScopeIds: string[] | null, optimized = false ): Node | null => { const isFragmentStart = isComment(node) && node.data === '[' const onMismatch = () => handleMismatch( node, vnode, parentComponent, parentSuspense, slotScopeIds, isFragmentStart ) const { type, ref, shapeFlag } = vnode const domType = node.nodeType vnode.el = node let nextNode: Node | null = null switch (type) { case Text: if (domType !== DOMNodeTypes.TEXT) { nextNode = onMismatch() } else { if ((node as Text).data !== vnode.children) { hasMismatch = true __DEV__ && warn( `Hydration text mismatch:` + `\n- Client: ${JSON.stringify((node as Text).data)}` + `\n- Server: ${JSON.stringify(vnode.children)}` ) ;(node as Text).data = vnode.children as string } nextNode = nextSibling(node) } break case Comment: if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) { nextNode = onMismatch() } else { nextNode = nextSibling(node) } break case Static: if (domType !== DOMNodeTypes.ELEMENT) { nextNode = onMismatch() } else { // determine anchor, adopt content nextNode = node // if the static vnode has its content stripped during build, // adopt it from the server-rendered HTML. const needToAdoptContent = !(vnode.children as string).length for (let i = 0; i < vnode.staticCount!; i++) { if (needToAdoptContent) vnode.children += (nextNode as Element).outerHTML if (i === vnode.staticCount! - 1) { vnode.anchor = nextNode } nextNode = nextSibling(nextNode)! } return nextNode } break case Fragment: if (!isFragmentStart) { nextNode = onMismatch() } else { nextNode = hydrateFragment( node as Comment, vnode, parentComponent, parentSuspense, slotScopeIds, optimized ) } break default: if (shapeFlag & ShapeFlags.ELEMENT) { if ( domType !== DOMNodeTypes.ELEMENT || (vnode.type as string).toLowerCase() !== (node as Element).tagName.toLowerCase() ) { nextNode = onMismatch() } else { nextNode = hydrateElement( node as Element, vnode, parentComponent, parentSuspense, slotScopeIds, 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. vnode.slotScopeIds = slotScopeIds const container = parentNode(node)! 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. nextNode = isFragmentStart ? locateClosingAsyncAnchor(node) : nextSibling(node) // #3787 // if component is async, it may get moved / unmounted before its // inner component is loaded, so we need to give it a placeholder // vnode that matches its adopted DOM. if (isAsyncWrapper(vnode)) { let subTree if (isFragmentStart) { subTree = createVNode(Fragment) subTree.anchor = nextNode ? nextNode.previousSibling : container.lastChild } else { subTree = node.nodeType === 3 ? createTextVNode('') : createVNode('div') } subTree.el = node vnode.component!.subTree = subTree } } else if (shapeFlag & ShapeFlags.TELEPORT) { if (domType !== DOMNodeTypes.COMMENT) { nextNode = onMismatch() } else { nextNode = (vnode.type as typeof TeleportImpl).hydrate( node, vnode as TeleportVNode, parentComponent, parentSuspense, slotScopeIds, optimized, rendererInternals, hydrateChildren ) } } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { nextNode = (vnode.type as typeof SuspenseImpl).hydrate( node, vnode, parentComponent, parentSuspense, isSVGContainer(parentNode(node)!), slotScopeIds, optimized, rendererInternals, hydrateNode ) } else if (__DEV__) { warn('Invalid HostVNode type:', type, `(${typeof type})`) } } if (ref != null) { setRef(ref, null, parentSuspense, vnode) } return nextNode } const hydrateElement = ( el: Element, vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, slotScopeIds: string[] | null, optimized: boolean ) => { optimized = optimized || !!vnode.dynamicChildren const { type, props, patchFlag, shapeFlag, dirs } = vnode // #4006 for form elements with non-string v-model value bindings // e.g.