hydration.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import {
  2. VNode,
  3. normalizeVNode,
  4. Text,
  5. Comment,
  6. Static,
  7. Fragment,
  8. Portal
  9. } from './vnode'
  10. import { queuePostFlushCb, flushPostFlushCbs } from './scheduler'
  11. import { ComponentInternalInstance } from './component'
  12. import { invokeDirectiveHook } from './directives'
  13. import { warn } from './warning'
  14. import {
  15. PatchFlags,
  16. ShapeFlags,
  17. isReservedProp,
  18. isOn,
  19. isString
  20. } from '@vue/shared'
  21. import { RendererOptions, MountComponentFn } from './renderer'
  22. export type RootHydrateFunction = (
  23. vnode: VNode<Node, Element>,
  24. container: Element
  25. ) => void
  26. // Note: hydration is DOM-specific
  27. // But we have to place it in core due to tight coupling with core - splitting
  28. // it out creates a ton of unnecessary complexity.
  29. // Hydration also depends on some renderer internal logic which needs to be
  30. // passed in via arguments.
  31. export function createHydrationFunctions(
  32. mountComponent: MountComponentFn<Node, Element>,
  33. patchProp: RendererOptions['patchProp']
  34. ) {
  35. const hydrate: RootHydrateFunction = (vnode, container) => {
  36. if (__DEV__ && !container.hasChildNodes()) {
  37. warn(`Attempting to hydrate existing markup but container is empty.`)
  38. return
  39. }
  40. hydrateNode(container.firstChild!, vnode)
  41. flushPostFlushCbs()
  42. }
  43. // TODO handle mismatches
  44. const hydrateNode = (
  45. node: Node,
  46. vnode: VNode,
  47. parentComponent: ComponentInternalInstance | null = null
  48. ): Node | null | undefined => {
  49. const { type, shapeFlag } = vnode
  50. vnode.el = node
  51. switch (type) {
  52. case Text:
  53. case Comment:
  54. case Static:
  55. return node.nextSibling
  56. case Fragment:
  57. const anchor = (vnode.anchor = hydrateChildren(
  58. node.nextSibling,
  59. vnode.children as VNode[],
  60. parentComponent
  61. )!)
  62. // TODO handle potential hydration error if fragment didn't get
  63. // back anchor as expected.
  64. return anchor.nextSibling
  65. case Portal:
  66. hydratePortal(vnode, parentComponent)
  67. return node.nextSibling
  68. default:
  69. if (shapeFlag & ShapeFlags.ELEMENT) {
  70. return hydrateElement(node as Element, vnode, parentComponent)
  71. } else if (shapeFlag & ShapeFlags.COMPONENT) {
  72. // when setting up the render effect, if the initial vnode already
  73. // has .el set, the component will perform hydration instead of mount
  74. // on its sub-tree.
  75. mountComponent(vnode, null, null, parentComponent, null, false)
  76. const subTree = vnode.component!.subTree
  77. return (subTree.anchor || subTree.el).nextSibling
  78. } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
  79. // TODO Suspense
  80. } else if (__DEV__) {
  81. warn('Invalid HostVNode type:', type, `(${typeof type})`)
  82. }
  83. }
  84. }
  85. const hydrateElement = (
  86. el: Element,
  87. vnode: VNode,
  88. parentComponent: ComponentInternalInstance | null
  89. ) => {
  90. const { props, patchFlag } = vnode
  91. // skip props & children if this is hoisted static nodes
  92. if (patchFlag !== PatchFlags.HOISTED) {
  93. // props
  94. if (props !== null) {
  95. if (
  96. patchFlag & PatchFlags.FULL_PROPS ||
  97. patchFlag & PatchFlags.HYDRATE_EVENTS
  98. ) {
  99. for (const key in props) {
  100. if (!isReservedProp(key) && isOn(key)) {
  101. patchProp(el, key, props[key], null)
  102. }
  103. }
  104. } else if (props.onClick != null) {
  105. // Fast path for click listeners (which is most often) to avoid
  106. // iterating through props.
  107. patchProp(el, 'onClick', props.onClick, null)
  108. }
  109. // vnode hooks
  110. const { onVnodeBeforeMount, onVnodeMounted } = props
  111. if (onVnodeBeforeMount != null) {
  112. invokeDirectiveHook(onVnodeBeforeMount, parentComponent, vnode)
  113. }
  114. if (onVnodeMounted != null) {
  115. queuePostFlushCb(() => {
  116. invokeDirectiveHook(onVnodeMounted, parentComponent, vnode)
  117. })
  118. }
  119. }
  120. // children
  121. if (
  122. vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
  123. // skip if element has innerHTML / textContent
  124. !(props !== null && (props.innerHTML || props.textContent))
  125. ) {
  126. hydrateChildren(
  127. el.firstChild,
  128. vnode.children as VNode[],
  129. parentComponent
  130. )
  131. }
  132. }
  133. return el.nextSibling
  134. }
  135. const hydrateChildren = (
  136. node: Node | null | undefined,
  137. vnodes: VNode[],
  138. parentComponent: ComponentInternalInstance | null
  139. ): Node | null | undefined => {
  140. for (let i = 0; node != null && i < vnodes.length; i++) {
  141. // TODO can skip normalizeVNode in optimized mode
  142. // (need hint on rendered markup?)
  143. const vnode = (vnodes[i] = normalizeVNode(vnodes[i]))
  144. node = hydrateNode(node, vnode, parentComponent)
  145. }
  146. return node
  147. }
  148. const hydratePortal = (
  149. vnode: VNode,
  150. parentComponent: ComponentInternalInstance | null
  151. ) => {
  152. const targetSelector = vnode.props && vnode.props.target
  153. const target = (vnode.target = isString(targetSelector)
  154. ? document.querySelector(targetSelector)
  155. : targetSelector)
  156. if (target != null && vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  157. hydrateChildren(
  158. target.firstChild,
  159. vnode.children as VNode[],
  160. parentComponent
  161. )
  162. }
  163. }
  164. return [hydrate, hydrateNode] as const
  165. }