import { VNode, normalizeVNode, Text, Comment, Static, Fragment, VNodeHook } from './vnode' import { flushPostFlushCbs } from './scheduler' import { ComponentOptions, ComponentInternalInstance } from './component' import { invokeDirectiveHook } from './directives' import { warn } from './warning' import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared' import { RendererInternals, invokeVNodeHook } from './renderer' import { SuspenseImpl, SuspenseBoundary, queueEffectWithSuspense } from './components/Suspense' import { TeleportImpl } from './components/Teleport' export type RootHydrateFunction = ( vnode: VNode, container: Element ) => 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 (__DEV__ && !container.hasChildNodes()) { warn( `Attempting to hydrate existing markup but container is empty. ` + `Performing full mount instead.` ) patch(null, vnode, container) return } hasMismatch = false hydrateNode(container.firstChild!, vnode, 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, optimized = false ): Node | null => { const isFragmentStart = isComment(node) && node.data === '[' const onMismatch = () => handleMismtach( node, vnode, parentComponent, parentSuspense, isFragmentStart ) const { type, shapeFlag } = vnode const domType = node.nodeType vnode.el = node switch (type) { case Text: if (domType !== DOMNodeTypes.TEXT) { return onMismatch() } if ((node as Text).data !== vnode.children) { hasMismatch = 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 nextSibling(node) case Comment: if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) { return onMismatch() } return nextSibling(node) case Static: if (domType !== DOMNodeTypes.ELEMENT) { return onMismatch() } return nextSibling(node) case Fragment: if (!isFragmentStart) { return onMismatch() } 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 onMismatch() } 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. const container = parentNode(node)! const hydrateComponent = () => { mountComponent( vnode, container, null, parentComponent, parentSuspense, isSVGContainer(container) ) } // async component const loadAsync = (vnode.type as ComponentOptions).__asyncLoader if (loadAsync) { loadAsync().then(hydrateComponent) } else { hydrateComponent() } // 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. return isFragmentStart ? locateClosingAsyncAnchor(node) : nextSibling(node) } else if (shapeFlag & ShapeFlags.TELEPORT) { if (domType !== DOMNodeTypes.COMMENT) { return onMismatch() } return (vnode.type as typeof TeleportImpl).hydrate( node, vnode, parentComponent, parentSuspense, optimized, rendererInternals, hydrateChildren ) } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.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})`) } return null } } const hydrateElement = ( el: Element, vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, optimized: boolean ) => { optimized = optimized || !!vnode.dynamicChildren const { props, patchFlag, shapeFlag, dirs } = vnode // skip props & children if this is hoisted static nodes if (patchFlag !== PatchFlags.HOISTED) { // props if (props) { if ( !optimized || (patchFlag & PatchFlags.FULL_PROPS || patchFlag & PatchFlags.HYDRATE_EVENTS) ) { for (const key in props) { if (!isReservedProp(key) && isOn(key)) { patchProp(el, key, null, props[key]) } } } else if (props.onClick) { // Fast path for click listeners (which is most often) to avoid // iterating through props. patchProp(el, 'onClick', null, props.onClick) } } // vnode / directive hooks let vnodeHooks: VNodeHook | null | undefined if ((vnodeHooks = props && props.onVnodeBeforeMount)) { invokeVNodeHook(vnodeHooks, parentComponent, vnode) } if (dirs) { invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount') } if ((vnodeHooks = props && props.onVnodeMounted) || dirs) { queueEffectWithSuspense(() => { vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode) dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted') }, parentSuspense) } // children if ( shapeFlag & ShapeFlags.ARRAY_CHILDREN && // skip if element has innerHTML / textContent !(props && (props.innerHTML || props.textContent)) ) { let next = hydrateChildren( el.firstChild, vnode, el, parentComponent, parentSuspense, optimized ) let hasWarned = false while (next) { hasMismatch = true if (__DEV__ && !hasWarned) { warn( `Hydration children mismatch in <${vnode.type as string}>: ` + `server rendered element contains more child nodes than client vdom.` ) hasWarned = true } // The SSRed DOM contains more nodes than it should. Remove them. const cur = next next = next.nextSibling remove(cur) } } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { if (el.textContent !== vnode.children) { hasMismatch = true __DEV__ && warn( `Hydration text content mismatch in <${vnode.type as string}>:\n` + `- Client: ${el.textContent}\n` + `- Server: ${vnode.children as string}` ) el.textContent = vnode.children as string } } } return el.nextSibling } const hydrateChildren = ( node: Node | null, vnode: VNode, container: Element, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, optimized: boolean ): Node | null => { optimized = optimized || !!vnode.dynamicChildren const children = vnode.children as VNode[] const l = children.length let hasWarned = false for (let i = 0; i < l; i++) { const vnode = optimized ? children[i] : (children[i] = normalizeVNode(children[i])) if (node) { node = hydrateNode( node, vnode, parentComponent, parentSuspense, optimized ) } else { hasMismatch = true if (__DEV__ && !hasWarned) { warn( `Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` + `server rendered element contains fewer child nodes than client vdom.` ) hasWarned = true } // the SSRed DOM didn't contain enough nodes. Mount the missing ones. patch( null, vnode, container, null, parentComponent, parentSuspense, isSVGContainer(container) ) } } return node } const hydrateFragment = ( node: Comment, vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, optimized: boolean ) => { const container = parentNode(node)! const next = hydrateChildren( nextSibling(node)!, vnode, container, parentComponent, parentSuspense, optimized ) if (next && isComment(next) && next.data === ']') { return nextSibling((vnode.anchor = next)) } else { // fragment didn't hydrate successfully, since we didn't get a end anchor // back. This should have led to node/children mismatch warnings. hasMismatch = true // since the anchor is missing, we need to create one and insert it insert((vnode.anchor = createComment(`]`)), container, next) return next } } const handleMismtach = ( node: Node, vnode: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isFragment: boolean ) => { hasMismatch = true __DEV__ && warn( `Hydration node mismatch:\n- Client vnode:`, vnode.type, `\n- Server rendered DOM:`, node, node.nodeType === DOMNodeTypes.TEXT ? `(text)` : isComment(node) && node.data === '[' ? `(start of fragment)` : `` ) vnode.el = null if (isFragment) { // remove excessive fragment nodes const end = locateClosingAsyncAnchor(node) while (true) { const next = nextSibling(node) if (next && next !== end) { remove(next) } else { break } } } const next = nextSibling(node) const container = parentNode(node)! remove(node) 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 === '[') match++ if (node.data === ']') { if (match === 0) { return nextSibling(node) } else { match-- } } } } return node } return [hydrate, hydrateNode] as const }