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

152 lines
4.8 KiB
TypeScript
Raw Normal View History

2020-02-14 12:31:03 +08:00
import {
VNode,
normalizeVNode,
Text,
Comment,
Static,
Fragment,
Portal
} from './vnode'
import { queuePostFlushCb, flushPostFlushCbs } from './scheduler'
import { ComponentInternalInstance } from './component'
import { invokeDirectiveHook } from './directives'
import { warn } from './warning'
import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared'
import { RendererOptions, MountComponentFn } from './renderer'
2020-02-14 12:31:03 +08:00
export type RootHydrateFunction = (
vnode: VNode<Node, Element>,
container: Element
) => void
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(
mountComponent: MountComponentFn<Node, Element>,
patchProp: RendererOptions['patchProp']
2020-02-14 12:31:03 +08:00
) {
const hydrate: RootHydrateFunction = (vnode, container) => {
2020-02-14 12:31:03 +08:00
if (__DEV__ && !container.hasChildNodes()) {
warn(`Attempting to hydrate existing markup but container is empty.`)
return
}
hydrateNode(container.firstChild!, vnode)
flushPostFlushCbs()
}
// TODO handle mismatches
// TODO SVG
// TODO Suspense
const hydrateNode = (
2020-02-14 12:31:03 +08:00
node: Node,
vnode: VNode,
parentComponent: ComponentInternalInstance | null = null
): Node | null | undefined => {
2020-02-14 12:31:03 +08:00
const { type, shapeFlag } = vnode
vnode.el = node
switch (type) {
case Text:
case Comment:
case Static:
return node.nextSibling
case Fragment:
const anchor = (vnode.anchor = hydrateChildren(
node.nextSibling,
vnode.children as VNode[],
parentComponent
)!)
// TODO handle potential hydration error if fragment didn't get
// back anchor as expected.
return anchor.nextSibling
case Portal:
// TODO
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
return hydrateElement(node as Element, vnode, parentComponent)
} 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.
2020-02-14 12:31:03 +08:00
mountComponent(vnode, null, null, parentComponent, null, false)
const subTree = vnode.component!.subTree
return (subTree.anchor || subTree.el).nextSibling
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// TODO
} else if (__DEV__) {
warn('Invalid HostVNode type:', type, `(${typeof type})`)
}
}
}
const hydrateElement = (
2020-02-14 12:31:03 +08:00
el: Element,
vnode: VNode,
parentComponent: ComponentInternalInstance | null
) => {
2020-02-14 12:31:03 +08:00
const { props, patchFlag } = vnode
// skip props & children if this is hoisted static nodes
if (patchFlag !== PatchFlags.HOISTED) {
// props
if (props !== null) {
if (
patchFlag & PatchFlags.FULL_PROPS ||
patchFlag & PatchFlags.HYDRATE_EVENTS
) {
for (const key in props) {
if (!isReservedProp(key) && isOn(key)) {
patchProp(el, key, props[key], null)
}
}
} else if (props.onClick != null) {
// Fast path for click listeners (which is most often) to avoid
// iterating through props.
patchProp(el, 'onClick', props.onClick, null)
}
// vnode hooks
const { onVnodeBeforeMount, onVnodeMounted } = props
if (onVnodeBeforeMount != null) {
invokeDirectiveHook(onVnodeBeforeMount, parentComponent, vnode)
}
if (onVnodeMounted != null) {
queuePostFlushCb(() => {
invokeDirectiveHook(onVnodeMounted, parentComponent, vnode)
})
}
}
// children
if (
vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
// skip if element has innerHTML / textContent
!(props !== null && (props.innerHTML || props.textContent))
) {
hydrateChildren(
el.firstChild,
vnode.children as VNode[],
parentComponent
)
}
}
return el.nextSibling
}
const hydrateChildren = (
2020-02-14 12:31:03 +08:00
node: Node | null | undefined,
vnodes: VNode[],
parentComponent: ComponentInternalInstance | null
): Node | null | undefined => {
2020-02-14 12:31:03 +08:00
for (let i = 0; node != null && i < vnodes.length; i++) {
// TODO can skip normalizeVNode in optimized mode
// (need hint on rendered markup?)
const vnode = (vnodes[i] = normalizeVNode(vnodes[i]))
node = hydrateNode(node, vnode, parentComponent)
}
return node
}
return [hydrate, hydrateNode] as const
}