vue3-yuanma/packages/runtime-core/src/hydration.ts

428 lines
12 KiB
TypeScript
Raw Normal View History

2020-02-16 00:40:09 +08:00
import { VNode, normalizeVNode, Text, Comment, Static, Fragment } from './vnode'
import { flushPostFlushCbs } from './scheduler'
2020-02-14 12:31:03 +08:00
import { ComponentInternalInstance } from './component'
import { invokeDirectiveHook } from './directives'
import { warn } from './warning'
2020-02-15 10:04:08 +08:00
import {
PatchFlags,
ShapeFlags,
isReservedProp,
isOn,
isString
} from '@vue/shared'
2020-02-16 00:40:09 +08:00
import { RendererInternals } from './renderer'
import {
SuspenseImpl,
SuspenseBoundary,
queueEffectWithSuspense
} from './components/Suspense'
2020-02-14 12:31:03 +08:00
export type RootHydrateFunction = (
vnode: VNode<Node, Element>,
container: Element
) => void
2020-03-04 05:12:38 +08:00
const enum DOMNodeTypes {
ELEMENT = 1,
TEXT = 3,
COMMENT = 8
}
2020-03-05 07:06:50 +08:00
let hasMismatch = false
2020-03-04 05:12:38 +08:00
const isSVGContainer = (container: Element) =>
/svg/.test(container.namespaceURI!) && container.tagName !== 'foreignObject'
const isComment = (node: Node): node is Comment =>
node.nodeType === DOMNodeTypes.COMMENT
2020-02-14 12:31:03 +08:00
// 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,
n: next,
o: { patchProp, nextSibling, parentNode }
} = rendererInternals
const hydrate: RootHydrateFunction = (vnode, container) => {
2020-02-14 12:31:03 +08:00
if (__DEV__ && !container.hasChildNodes()) {
2020-03-04 05:12:38 +08:00
warn(
`Attempting to hydrate existing markup but container is empty. ` +
`Performing full mount instead.`
)
patch(null, vnode, container)
2020-02-14 12:31:03 +08:00
return
}
2020-03-05 07:06:50 +08:00
hasMismatch = false
hydrateNode(container.firstChild!, vnode, null, null)
2020-02-14 12:31:03 +08:00
flushPostFlushCbs()
2020-03-05 07:06:50 +08:00
if (hasMismatch && !__TEST__) {
2020-03-04 05:12:38 +08:00
// this error should show up in production
console.error(`Hydration completed but contains mismatches.`)
}
2020-02-14 12:31:03 +08:00
}
const hydrateNode = (
2020-02-14 12:31:03 +08:00
node: Node,
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized = false
): Node | null => {
const isFragmentStart = isComment(node) && node.data === '1'
if (__DEV__ && isFragmentStart) {
// in dev mode, replace comment anchors with invisible text nodes
// for easier debugging
node = replaceAnchor(node, parentNode(node)!)
}
2020-02-14 12:31:03 +08:00
const { type, shapeFlag } = vnode
2020-03-04 05:12:38 +08:00
const domType = node.nodeType
2020-02-14 12:31:03 +08:00
vnode.el = node
2020-03-04 05:12:38 +08:00
2020-02-14 12:31:03 +08:00
switch (type) {
case Text:
2020-03-04 05:12:38 +08:00
if (domType !== DOMNodeTypes.TEXT) {
return handleMismtach(node, vnode, parentComponent, parentSuspense)
2020-03-04 05:12:38 +08:00
}
if ((node as Text).data !== vnode.children) {
2020-03-05 07:06:50 +08:00
hasMismatch = true
2020-03-04 05:12:38 +08:00
__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)
2020-02-14 12:31:03 +08:00
case Comment:
2020-03-04 05:12:38 +08:00
if (domType !== DOMNodeTypes.COMMENT) {
return handleMismtach(node, vnode, parentComponent, parentSuspense)
2020-03-04 05:12:38 +08:00
}
return nextSibling(node)
2020-02-14 12:31:03 +08:00
case Static:
2020-03-04 05:12:38 +08:00
if (domType !== DOMNodeTypes.ELEMENT) {
return handleMismtach(node, vnode, parentComponent, parentSuspense)
2020-03-04 05:12:38 +08:00
}
return nextSibling(node)
2020-02-14 12:31:03 +08:00
case Fragment:
if (domType !== (__DEV__ ? DOMNodeTypes.TEXT : DOMNodeTypes.COMMENT)) {
return handleMismtach(node, vnode, parentComponent, parentSuspense)
}
return hydrateFragment(
node as Comment,
vnode,
parentComponent,
parentSuspense,
optimized
)
2020-02-14 12:31:03 +08:00
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
if (
domType !== DOMNodeTypes.ELEMENT ||
vnode.type !== (node as Element).tagName.toLowerCase()
) {
return handleMismtach(node, vnode, parentComponent, parentSuspense)
2020-03-04 05:12:38 +08:00
}
return hydrateElement(
node as Element,
vnode,
parentComponent,
parentSuspense,
optimized
)
2020-02-14 12:31:03 +08:00
} 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)!
mountComponent(
vnode,
container,
null,
parentComponent,
parentSuspense,
isSVGContainer(container)
)
2020-02-14 12:31:03 +08:00
const subTree = vnode.component!.subTree
if (subTree) {
return next(subTree)
} else {
// no subTree means this is an async component
// try to locate the ending node
return isFragmentStart
? locateClosingAsyncAnchor(node)
: nextSibling(node)
}
2020-02-16 00:40:09 +08:00
} else if (shapeFlag & ShapeFlags.PORTAL) {
2020-03-04 05:12:38 +08:00
if (domType !== DOMNodeTypes.COMMENT) {
return handleMismtach(node, vnode, parentComponent, parentSuspense)
2020-03-04 05:12:38 +08:00
}
hydratePortal(vnode, parentComponent, parentSuspense, optimized)
return nextSibling(node)
2020-02-14 12:31:03 +08:00
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
return (vnode.type as typeof SuspenseImpl).hydrate(
node,
vnode,
parentComponent,
parentSuspense,
isSVGContainer(parentNode(node)!),
optimized,
rendererInternals,
hydrateNode
)
2020-02-14 12:31:03 +08:00
} else if (__DEV__) {
warn('Invalid HostVNode type:', type, `(${typeof type})`)
}
return null
2020-02-14 12:31:03 +08:00
}
}
const hydrateElement = (
2020-02-14 12:31:03 +08:00
el: Element,
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized: boolean
) => {
2020-03-06 00:29:50 +08:00
optimized = optimized || vnode.dynamicChildren !== null
2020-03-04 05:12:38 +08:00
const { props, patchFlag, shapeFlag } = vnode
2020-02-14 12:31:03 +08:00
// skip props & children if this is hoisted static nodes
if (patchFlag !== PatchFlags.HOISTED) {
// props
if (props !== null) {
if (
2020-03-06 00:29:50 +08:00
!optimized ||
(patchFlag & PatchFlags.FULL_PROPS ||
patchFlag & PatchFlags.HYDRATE_EVENTS)
2020-02-14 12:31:03 +08:00
) {
for (const key in props) {
if (!isReservedProp(key) && isOn(key)) {
patchProp(el, key, null, props[key])
2020-02-14 12:31:03 +08:00
}
}
} else if (props.onClick != null) {
// Fast path for click listeners (which is most often) to avoid
// iterating through props.
patchProp(el, 'onClick', null, props.onClick)
2020-02-14 12:31:03 +08:00
}
// vnode hooks
const { onVnodeBeforeMount, onVnodeMounted } = props
if (onVnodeBeforeMount != null) {
invokeDirectiveHook(onVnodeBeforeMount, parentComponent, vnode)
}
if (onVnodeMounted != null) {
queueEffectWithSuspense(() => {
2020-02-14 12:31:03 +08:00
invokeDirectiveHook(onVnodeMounted, parentComponent, vnode)
}, parentSuspense)
2020-02-14 12:31:03 +08:00
}
}
// children
if (
2020-03-04 05:12:38 +08:00
shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
2020-02-14 12:31:03 +08:00
// skip if element has innerHTML / textContent
!(props !== null && (props.innerHTML || props.textContent))
) {
2020-03-04 05:12:38 +08:00
let next = hydrateChildren(
2020-02-14 12:31:03 +08:00
el.firstChild,
vnode,
2020-03-04 05:12:38 +08:00
el,
parentComponent,
parentSuspense,
2020-03-06 00:29:50 +08:00
optimized
2020-02-14 12:31:03 +08:00
)
let hasWarned = false
2020-03-04 05:12:38 +08:00
while (next) {
2020-03-05 07:06:50 +08:00
hasMismatch = true
if (__DEV__ && !hasWarned) {
2020-03-04 05:12:38 +08:00
warn(
`Hydration children mismatch in <${vnode.type as string}>: ` +
2020-03-04 05:12:38 +08:00
`server rendered element contains more child nodes than client vdom.`
)
hasWarned = true
}
2020-03-04 05:12:38 +08:00
// 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) {
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
}
2020-02-14 12:31:03 +08:00
}
}
return el.nextSibling
}
const hydrateChildren = (
node: Node | null,
vnode: VNode,
2020-03-04 05:12:38 +08:00
container: Element,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized: boolean
): Node | null => {
optimized = optimized || vnode.dynamicChildren !== null
2020-03-05 07:06:50 +08:00
const children = vnode.children as VNode[]
const l = children.length
let hasWarned = false
2020-03-05 07:06:50 +08:00
for (let i = 0; i < l; i++) {
const vnode = optimized
? children[i]
: (children[i] = normalizeVNode(children[i]))
2020-03-04 05:12:38 +08:00
if (node) {
node = hydrateNode(
node,
vnode,
parentComponent,
parentSuspense,
optimized
)
2020-03-04 05:12:38 +08:00
} else {
2020-03-05 07:06:50 +08:00
hasMismatch = true
if (__DEV__ && !hasWarned) {
2020-03-04 05:12:38 +08:00
warn(
`Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` +
2020-03-04 05:12:38 +08:00
`server rendered element contains fewer child nodes than client vdom.`
)
hasWarned = true
}
2020-03-04 05:12:38 +08:00
// the SSRed DOM didn't contain enough nodes. Mount the missing ones.
patch(
null,
vnode,
container,
null,
parentComponent,
parentSuspense,
isSVGContainer(container)
)
2020-03-04 05:12:38 +08:00
}
2020-02-14 12:31:03 +08:00
}
return node
}
const hydrateFragment = (
node: Comment,
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized: boolean
) => {
const container = parentNode(node)!
let next = hydrateChildren(
nextSibling(node)!,
vnode,
container,
parentComponent,
parentSuspense,
optimized
)!
if (__DEV__) {
next = replaceAnchor(next, container)
}
return nextSibling((vnode.anchor = next))
}
2020-02-15 10:04:08 +08:00
const hydratePortal = (
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized: boolean
2020-02-15 10:04:08 +08:00
) => {
const targetSelector = vnode.props && vnode.props.target
const target = (vnode.target = isString(targetSelector)
? document.querySelector(targetSelector)
: targetSelector)
if (target != null && vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
2020-03-04 05:12:38 +08:00
hydrateChildren(
target.firstChild,
vnode,
2020-03-05 07:06:50 +08:00
target,
2020-03-04 05:12:38 +08:00
parentComponent,
parentSuspense,
2020-03-04 05:12:38 +08:00
optimized
)
2020-03-05 07:06:50 +08:00
} else if (__DEV__) {
warn(
`Attempting to hydrate portal but target ${targetSelector} does not ` +
`exist in server-rendered markup.`
)
2020-02-15 10:04:08 +08:00
}
}
2020-03-04 05:12:38 +08:00
const handleMismtach = (
node: Node,
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null
2020-03-04 05:12:38 +08:00
) => {
2020-03-05 07:06:50 +08:00
hasMismatch = true
2020-03-04 05:12:38 +08:00
__DEV__ &&
warn(
`Hydration node mismatch:\n- Client vnode:`,
vnode.type,
`\n- Server rendered DOM:`,
node,
node.nodeType === DOMNodeTypes.TEXT ? `(text)` : ``
2020-03-04 05:12:38 +08:00
)
vnode.el = null
const next = nextSibling(node)
const container = parentNode(node)!
2020-03-04 05:12:38 +08:00
container.removeChild(node)
patch(
null,
vnode,
container,
next,
parentComponent,
parentSuspense,
isSVGContainer(container)
)
2020-03-04 05:12:38 +08:00
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
}
const replaceAnchor = (node: Node, parent: Element): Node => {
const text = document.createTextNode('')
parent.insertBefore(text, node)
parent.removeChild(node)
return text
}
2020-02-14 12:31:03 +08:00
return [hydrate, hydrateNode] as const
}