| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- 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, setRef } from './renderer'
- import {
- SuspenseImpl,
- SuspenseBoundary,
- queueEffectWithSuspense
- } from './components/Suspense'
- import { TeleportImpl, TeleportVNode } from './components/Teleport'
- export type RootHydrateFunction = (
- vnode: VNode<Node, Element>,
- 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<Node, Element>
- ) {
- 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 = () =>
- handleMismatch(
- node,
- vnode,
- parentComponent,
- parentSuspense,
- 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,
- optimized
- )
- }
- break
- default:
- if (shapeFlag & ShapeFlags.ELEMENT) {
- if (
- domType !== DOMNodeTypes.ELEMENT ||
- vnode.type !== (node as Element).tagName.toLowerCase()
- ) {
- nextNode = onMismatch()
- } else {
- nextNode = 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),
- optimized
- )
- }
- // 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.
- nextNode = isFragmentStart
- ? locateClosingAsyncAnchor(node)
- : nextSibling(node)
- } 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,
- optimized,
- rendererInternals,
- hydrateChildren
- )
- }
- } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
- nextNode = (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})`)
- }
- }
- if (ref != null && parentComponent) {
- setRef(ref, null, parentComponent, parentSuspense, vnode)
- }
- return nextNode
- }
- 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) {
- if (dirs) {
- invokeDirectiveHook(vnode, null, parentComponent, 'created')
- }
- // 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,
- parentVNode: VNode,
- container: Element,
- parentComponent: ComponentInternalInstance | null,
- parentSuspense: SuspenseBoundary | null,
- optimized: boolean
- ): Node | null => {
- optimized = optimized || !!parentVNode.dynamicChildren
- const children = parentVNode.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 handleMismatch = (
- node: Node,
- vnode: VNode,
- parentComponent: ComponentInternalInstance | null,
- parentSuspense: SuspenseBoundary | null,
- isFragment: boolean
- ): Node | null => {
- 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
- }
|