import { InjectionKey } from './apiInject'
import { isFunction, NO, isObject } from '@vue/shared'
import { warn } from './warning'
-import { createVNode, cloneVNode } from './vnode'
+import { createVNode, cloneVNode, VNode } from './vnode'
export interface App<HostElement = any> {
config: AppConfig
component(name: string, component: Component): this
directive(name: string): Directive | undefined
directive(name: string, directive: Directive): this
- mount(rootContainer: HostElement | string): ComponentPublicInstance
+ mount(
+ rootContainer: HostElement | string,
+ isHydrate?: boolean
+ ): ComponentPublicInstance
unmount(rootContainer: HostElement | string): void
provide<T>(key: InjectionKey<T> | string, value: T): this
) => App<HostElement>
export function createAppAPI<HostNode, HostElement>(
- render: RootRenderFunction<HostNode, HostElement>
+ render: RootRenderFunction<HostNode, HostElement>,
+ hydrate: (vnode: VNode, container: Element) => void
): CreateAppFunction<HostElement> {
return function createApp(rootComponent: Component, rootProps = null) {
if (rootProps != null && !isObject(rootProps)) {
return app
},
- mount(rootContainer: HostElement): any {
+ mount(rootContainer: HostElement, isHydrate?: boolean): any {
if (!isMounted) {
const vnode = createVNode(rootComponent, rootProps)
// store app context on the root VNode.
}
}
- render(vnode, rootContainer)
+ if (isHydrate) {
+ hydrate(vnode, rootContainer as any)
+ } else {
+ render(vnode, rootContainer)
+ }
isMounted = true
app._container = rootContainer
return vnode.component!.proxy
instance: ComponentInternalInstance,
setupRenderEffect: (
instance: ComponentInternalInstance,
- parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
initialVNode: VNode<HostNode, HostElement>,
- container: HostElement,
+ container: HostElement | null,
anchor: HostNode | null,
+ parentSuspense: SuspenseBoundary<HostNode, HostElement> | null,
isSVG: boolean
) => void
): void
handleSetupResult(instance, asyncSetupResult, suspense)
setupRenderEffect(
instance,
- suspense,
vnode,
// component may have been moved before resolve
parentNode(instance.subTree.el)!,
next(instance.subTree),
+ suspense,
isSVG
)
updateHOCHostEl(instance, vnode.el)
--- /dev/null
+import {
+ VNode,
+ normalizeVNode,
+ Text,
+ Comment,
+ Static,
+ Fragment,
+ Portal
+} from './vnode'
+import { queuePostFlushCb, flushPostFlushCbs } from './scheduler'
+import { ComponentInternalInstance } from './component'
+import { invokeDirectiveHook } from './directives'
+import { ShapeFlags } from './shapeFlags'
+import { warn } from './warning'
+import { PatchFlags, isReservedProp, isOn } from '@vue/shared'
+
+// Note: hydration is DOM-specific
+// but we have to place it in core due to tight coupling with core renderer
+// logic - splitting it out
+export function createHydrateFn(
+ mountComponent: any, // TODO
+ patchProp: any // TODO
+) {
+ function hydrate(vnode: VNode, container: Element) {
+ 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
+ function hydrateNode(
+ node: Node,
+ vnode: VNode,
+ parentComponent: ComponentInternalInstance | null = null
+ ): Node | null | undefined {
+ 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) {
+ 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})`)
+ }
+ }
+ }
+
+ function hydrateElement(
+ el: Element,
+ vnode: VNode,
+ parentComponent: ComponentInternalInstance | null
+ ) {
+ 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
+ }
+
+ function hydrateChildren(
+ node: Node | null | undefined,
+ vnodes: VNode[],
+ parentComponent: ComponentInternalInstance | null
+ ): Node | null | undefined {
+ 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
+}
openBlock,
createBlock
} from './vnode'
-// VNode type symbols
-export { Text, Comment, Fragment, Portal } from './vnode'
// Internal Components
+export { Fragment, Portal } from './vnode'
export { Suspense, SuspenseProps } from './components/Suspense'
export { KeepAlive, KeepAliveProps } from './components/KeepAlive'
export {
isReservedProp,
isFunction,
PatchFlags,
- NOOP,
- isOn
+ NOOP
} from '@vue/shared'
import {
queueJob,
import { pushWarningContext, popWarningContext, warn } from './warning'
import { invokeDirectiveHook } from './directives'
import { ComponentPublicInstance } from './componentProxy'
-import { createAppAPI, CreateAppFunction } from './apiCreateApp'
+import { createAppAPI } from './apiCreateApp'
import {
SuspenseBoundary,
queueEffectWithSuspense,
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
import { KeepAliveSink, isKeepAlive } from './components/KeepAlive'
import { registerHMR, unregisterHMR } from './hmr'
+import { createHydrateFn } from './hydration'
const __HMR__ = __BUNDLER__ && __DEV__
export function createRenderer<
HostNode extends object = any,
HostElement extends HostNode = any
->(
- options: RendererOptions<HostNode, HostElement>
-): {
- render: RootRenderFunction<HostNode, HostElement>
- hydrate: RootRenderFunction<HostNode, HostElement>
- createApp: CreateAppFunction<HostElement>
-} {
+>(options: RendererOptions<HostNode, HostElement>) {
type HostVNode = VNode<HostNode, HostElement>
type HostVNodeChildren = VNodeArrayChildren<HostNode, HostElement>
type HostSuspenseBoundary = SuspenseBoundary<HostNode, HostElement>
function mountComponent(
initialVNode: HostVNode,
- container: HostElement,
+ container: HostElement | null, // only null during hydration
anchor: HostNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: HostSuspenseBoundary | null,
parentSuspense.registerDep(instance, setupRenderEffect)
- // give it a placeholder
+ // Give it a placeholder if this is not hydration
const placeholder = (instance.subTree = createVNode(Comment))
- processCommentNode(null, placeholder, container, anchor)
+ processCommentNode(null, placeholder, container!, anchor)
initialVNode.el = placeholder.el
return
}
setupRenderEffect(
instance,
- parentSuspense,
initialVNode,
container,
anchor,
+ parentSuspense,
isSVG
)
function setupRenderEffect(
instance: ComponentInternalInstance,
- parentSuspense: HostSuspenseBoundary | null,
initialVNode: HostVNode,
- container: HostElement,
+ container: HostElement | null, // only null during hydration
anchor: HostNode | null,
+ parentSuspense: HostSuspenseBoundary | null,
isSVG: boolean
) {
// create reactive effect for rendering
if (instance.bm !== null) {
invokeHooks(instance.bm)
}
- patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
- initialVNode.el = subTree.el
+ if (initialVNode.el) {
+ // vnode has adopted host node - perform hydration instead of mount.
+ hydrateNode(initialVNode.el as Node, subTree, instance)
+ } else {
+ patch(
+ null,
+ subTree,
+ container!, // container is only null during hydration
+ anchor,
+ instance,
+ parentSuspense,
+ isSVG
+ )
+ initialVNode.el = subTree.el
+ }
// mounted hook
if (instance.m !== null) {
queuePostRenderEffect(instance.m, parentSuspense)
container._vnode = vnode
}
- function hydrate(vnode: HostVNode, container: any) {
- hydrateNode(container.firstChild, vnode, container)
- flushPostFlushCbs()
- }
-
- // TODO handle mismatches
- function hydrateNode(
- node: any,
- vnode: HostVNode,
- container: any,
- parentComponent: ComponentInternalInstance | null = null
- ): any {
- const { type, shapeFlag } = vnode
- switch (type) {
- case Text:
- case Comment:
- case Static:
- vnode.el = node
- return node.nextSibling
- case Fragment:
- vnode.el = node
- const anchor = (vnode.anchor = hydrateChildren(
- node.nextSibling,
- vnode.children as HostVNode[],
- container,
- parentComponent
- ))
- return anchor.nextSibling
- case Portal:
- // TODO
- break
- default:
- if (shapeFlag & ShapeFlags.ELEMENT) {
- return hydrateElement(node, vnode, parentComponent)
- } else if (shapeFlag & ShapeFlags.COMPONENT) {
- // TODO
- } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
- // TODO
- } else if (__DEV__) {
- warn('Invalid HostVNode type:', type, `(${typeof type})`)
- }
- }
- }
-
- function hydrateElement(
- el: any,
- vnode: HostVNode,
- parentComponent: ComponentInternalInstance | null
- ) {
- vnode.el = el
- 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)) {
- hostPatchProp(el, key, props[key], null)
- }
- }
- } else if (props.onClick != null) {
- // Fast path for click listeners (which is most often) to avoid
- // iterating through props.
- hostPatchProp(el, 'onClick', props.onClick, null)
- }
- // vnode mounted hook
- const { onVnodeMounted } = props
- if (onVnodeMounted != null) {
- queuePostFlushCb(() => {
- invokeDirectiveHook(onVnodeMounted, parentComponent, vnode, null)
- })
- }
- }
- // 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 HostVNode[],
- el,
- parentComponent
- )
- }
- }
- return el.nextSibling
- }
-
- function hydrateChildren(
- node: any,
- vnodes: HostVNode[],
- container: any,
- parentComponent: ComponentInternalInstance | null = null
- ) {
- for (let i = 0; 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, container, parentComponent)
- }
- return node
- }
+ const [hydrate, hydrateNode] = createHydrateFn(mountComponent, hostPatchProp)
return {
render,
hydrate,
- createApp: createAppAPI(render)
+ createApp: createAppAPI<HostNode, HostElement>(render, hydrate)
}
}
}
const { mount } = app
- app.mount = (container): any => {
+ app.mount = (container: Element | string): any => {
if (isString(container)) {
container = document.querySelector(container)!
if (!container) {
) {
component.template = container.innerHTML
}
- // clear content before mounting
- container.innerHTML = ''
- return mount(container)
+ const isHydrate = container.hasAttribute('data-server-rendered')
+ if (!isHydrate) {
+ // clear content before mounting
+ container.innerHTML = ''
+ }
+ return mount(container, isHydrate)
}
return app