hydration.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. import {
  2. VNode,
  3. normalizeVNode,
  4. Text,
  5. Comment,
  6. Static,
  7. Fragment,
  8. VNodeHook
  9. } from './vnode'
  10. import { flushPostFlushCbs } from './scheduler'
  11. import { ComponentOptions, ComponentInternalInstance } from './component'
  12. import { invokeDirectiveHook } from './directives'
  13. import { warn } from './warning'
  14. import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared'
  15. import { RendererInternals, invokeVNodeHook, setRef } from './renderer'
  16. import {
  17. SuspenseImpl,
  18. SuspenseBoundary,
  19. queueEffectWithSuspense
  20. } from './components/Suspense'
  21. import { TeleportImpl } from './components/Teleport'
  22. export type RootHydrateFunction = (
  23. vnode: VNode<Node, Element>,
  24. container: Element
  25. ) => void
  26. const enum DOMNodeTypes {
  27. ELEMENT = 1,
  28. TEXT = 3,
  29. COMMENT = 8
  30. }
  31. let hasMismatch = false
  32. const isSVGContainer = (container: Element) =>
  33. /svg/.test(container.namespaceURI!) && container.tagName !== 'foreignObject'
  34. const isComment = (node: Node): node is Comment =>
  35. node.nodeType === DOMNodeTypes.COMMENT
  36. // Note: hydration is DOM-specific
  37. // But we have to place it in core due to tight coupling with core - splitting
  38. // it out creates a ton of unnecessary complexity.
  39. // Hydration also depends on some renderer internal logic which needs to be
  40. // passed in via arguments.
  41. export function createHydrationFunctions(
  42. rendererInternals: RendererInternals<Node, Element>
  43. ) {
  44. const {
  45. mt: mountComponent,
  46. p: patch,
  47. o: { patchProp, nextSibling, parentNode, remove, insert, createComment }
  48. } = rendererInternals
  49. const hydrate: RootHydrateFunction = (vnode, container) => {
  50. if (__DEV__ && !container.hasChildNodes()) {
  51. warn(
  52. `Attempting to hydrate existing markup but container is empty. ` +
  53. `Performing full mount instead.`
  54. )
  55. patch(null, vnode, container)
  56. return
  57. }
  58. hasMismatch = false
  59. hydrateNode(container.firstChild!, vnode, null, null)
  60. flushPostFlushCbs()
  61. if (hasMismatch && !__TEST__) {
  62. // this error should show up in production
  63. console.error(`Hydration completed but contains mismatches.`)
  64. }
  65. }
  66. const hydrateNode = (
  67. node: Node,
  68. vnode: VNode,
  69. parentComponent: ComponentInternalInstance | null,
  70. parentSuspense: SuspenseBoundary | null,
  71. optimized = false
  72. ): Node | null => {
  73. const isFragmentStart = isComment(node) && node.data === '['
  74. const onMismatch = () =>
  75. handleMismtach(
  76. node,
  77. vnode,
  78. parentComponent,
  79. parentSuspense,
  80. isFragmentStart
  81. )
  82. const { type, ref, shapeFlag } = vnode
  83. const domType = node.nodeType
  84. vnode.el = node
  85. let nextNode: Node | null = null
  86. switch (type) {
  87. case Text:
  88. if (domType !== DOMNodeTypes.TEXT) {
  89. nextNode = onMismatch()
  90. } else {
  91. if ((node as Text).data !== vnode.children) {
  92. hasMismatch = true
  93. __DEV__ &&
  94. warn(
  95. `Hydration text mismatch:` +
  96. `\n- Client: ${JSON.stringify((node as Text).data)}` +
  97. `\n- Server: ${JSON.stringify(vnode.children)}`
  98. )
  99. ;(node as Text).data = vnode.children as string
  100. }
  101. nextNode = nextSibling(node)
  102. }
  103. break
  104. case Comment:
  105. if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) {
  106. nextNode = onMismatch()
  107. } else {
  108. nextNode = nextSibling(node)
  109. }
  110. break
  111. case Static:
  112. if (domType !== DOMNodeTypes.ELEMENT) {
  113. nextNode = onMismatch()
  114. } else {
  115. // determine anchor, adopt content
  116. nextNode = node
  117. // if the static vnode has its content stripped during build,
  118. // adopt it from the server-rendered HTML.
  119. const needToAdoptContent = !(vnode.children as string).length
  120. for (let i = 0; i < vnode.staticCount; i++) {
  121. if (needToAdoptContent)
  122. vnode.children += (nextNode as Element).outerHTML
  123. if (i === vnode.staticCount - 1) {
  124. vnode.anchor = nextNode
  125. }
  126. nextNode = nextSibling(nextNode)!
  127. }
  128. return nextNode
  129. }
  130. break
  131. case Fragment:
  132. if (!isFragmentStart) {
  133. nextNode = onMismatch()
  134. } else {
  135. nextNode = hydrateFragment(
  136. node as Comment,
  137. vnode,
  138. parentComponent,
  139. parentSuspense,
  140. optimized
  141. )
  142. }
  143. break
  144. default:
  145. if (shapeFlag & ShapeFlags.ELEMENT) {
  146. if (
  147. domType !== DOMNodeTypes.ELEMENT ||
  148. vnode.type !== (node as Element).tagName.toLowerCase()
  149. ) {
  150. nextNode = onMismatch()
  151. } else {
  152. nextNode = hydrateElement(
  153. node as Element,
  154. vnode,
  155. parentComponent,
  156. parentSuspense,
  157. optimized
  158. )
  159. }
  160. } else if (shapeFlag & ShapeFlags.COMPONENT) {
  161. // when setting up the render effect, if the initial vnode already
  162. // has .el set, the component will perform hydration instead of mount
  163. // on its sub-tree.
  164. const container = parentNode(node)!
  165. const hydrateComponent = () => {
  166. mountComponent(
  167. vnode,
  168. container,
  169. null,
  170. parentComponent,
  171. parentSuspense,
  172. isSVGContainer(container),
  173. optimized
  174. )
  175. }
  176. // async component
  177. const loadAsync = (vnode.type as ComponentOptions).__asyncLoader
  178. if (loadAsync) {
  179. loadAsync().then(hydrateComponent)
  180. } else {
  181. hydrateComponent()
  182. }
  183. // component may be async, so in the case of fragments we cannot rely
  184. // on component's rendered output to determine the end of the fragment
  185. // instead, we do a lookahead to find the end anchor node.
  186. nextNode = isFragmentStart
  187. ? locateClosingAsyncAnchor(node)
  188. : nextSibling(node)
  189. } else if (shapeFlag & ShapeFlags.TELEPORT) {
  190. if (domType !== DOMNodeTypes.COMMENT) {
  191. nextNode = onMismatch()
  192. } else {
  193. nextNode = (vnode.type as typeof TeleportImpl).hydrate(
  194. node,
  195. vnode,
  196. parentComponent,
  197. parentSuspense,
  198. optimized,
  199. rendererInternals,
  200. hydrateChildren
  201. )
  202. }
  203. } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
  204. nextNode = (vnode.type as typeof SuspenseImpl).hydrate(
  205. node,
  206. vnode,
  207. parentComponent,
  208. parentSuspense,
  209. isSVGContainer(parentNode(node)!),
  210. optimized,
  211. rendererInternals,
  212. hydrateNode
  213. )
  214. } else if (__DEV__) {
  215. warn('Invalid HostVNode type:', type, `(${typeof type})`)
  216. }
  217. }
  218. if (ref != null && parentComponent) {
  219. setRef(ref, null, parentComponent, vnode)
  220. }
  221. return nextNode
  222. }
  223. const hydrateElement = (
  224. el: Element,
  225. vnode: VNode,
  226. parentComponent: ComponentInternalInstance | null,
  227. parentSuspense: SuspenseBoundary | null,
  228. optimized: boolean
  229. ) => {
  230. optimized = optimized || !!vnode.dynamicChildren
  231. const { props, patchFlag, shapeFlag, dirs } = vnode
  232. // skip props & children if this is hoisted static nodes
  233. if (patchFlag !== PatchFlags.HOISTED) {
  234. // props
  235. if (props) {
  236. if (
  237. !optimized ||
  238. (patchFlag & PatchFlags.FULL_PROPS ||
  239. patchFlag & PatchFlags.HYDRATE_EVENTS)
  240. ) {
  241. for (const key in props) {
  242. if (!isReservedProp(key) && isOn(key)) {
  243. patchProp(el, key, null, props[key])
  244. }
  245. }
  246. } else if (props.onClick) {
  247. // Fast path for click listeners (which is most often) to avoid
  248. // iterating through props.
  249. patchProp(el, 'onClick', null, props.onClick)
  250. }
  251. }
  252. // vnode / directive hooks
  253. let vnodeHooks: VNodeHook | null | undefined
  254. if ((vnodeHooks = props && props.onVnodeBeforeMount)) {
  255. invokeVNodeHook(vnodeHooks, parentComponent, vnode)
  256. }
  257. if (dirs) {
  258. invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
  259. }
  260. if ((vnodeHooks = props && props.onVnodeMounted) || dirs) {
  261. queueEffectWithSuspense(() => {
  262. vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode)
  263. dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
  264. }, parentSuspense)
  265. }
  266. // children
  267. if (
  268. shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
  269. // skip if element has innerHTML / textContent
  270. !(props && (props.innerHTML || props.textContent))
  271. ) {
  272. let next = hydrateChildren(
  273. el.firstChild,
  274. vnode,
  275. el,
  276. parentComponent,
  277. parentSuspense,
  278. optimized
  279. )
  280. let hasWarned = false
  281. while (next) {
  282. hasMismatch = true
  283. if (__DEV__ && !hasWarned) {
  284. warn(
  285. `Hydration children mismatch in <${vnode.type as string}>: ` +
  286. `server rendered element contains more child nodes than client vdom.`
  287. )
  288. hasWarned = true
  289. }
  290. // The SSRed DOM contains more nodes than it should. Remove them.
  291. const cur = next
  292. next = next.nextSibling
  293. remove(cur)
  294. }
  295. } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  296. if (el.textContent !== vnode.children) {
  297. hasMismatch = true
  298. __DEV__ &&
  299. warn(
  300. `Hydration text content mismatch in <${vnode.type as string}>:\n` +
  301. `- Client: ${el.textContent}\n` +
  302. `- Server: ${vnode.children as string}`
  303. )
  304. el.textContent = vnode.children as string
  305. }
  306. }
  307. }
  308. return el.nextSibling
  309. }
  310. const hydrateChildren = (
  311. node: Node | null,
  312. vnode: VNode,
  313. container: Element,
  314. parentComponent: ComponentInternalInstance | null,
  315. parentSuspense: SuspenseBoundary | null,
  316. optimized: boolean
  317. ): Node | null => {
  318. optimized = optimized || !!vnode.dynamicChildren
  319. const children = vnode.children as VNode[]
  320. const l = children.length
  321. let hasWarned = false
  322. for (let i = 0; i < l; i++) {
  323. const vnode = optimized
  324. ? children[i]
  325. : (children[i] = normalizeVNode(children[i]))
  326. if (node) {
  327. node = hydrateNode(
  328. node,
  329. vnode,
  330. parentComponent,
  331. parentSuspense,
  332. optimized
  333. )
  334. } else {
  335. hasMismatch = true
  336. if (__DEV__ && !hasWarned) {
  337. warn(
  338. `Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` +
  339. `server rendered element contains fewer child nodes than client vdom.`
  340. )
  341. hasWarned = true
  342. }
  343. // the SSRed DOM didn't contain enough nodes. Mount the missing ones.
  344. patch(
  345. null,
  346. vnode,
  347. container,
  348. null,
  349. parentComponent,
  350. parentSuspense,
  351. isSVGContainer(container)
  352. )
  353. }
  354. }
  355. return node
  356. }
  357. const hydrateFragment = (
  358. node: Comment,
  359. vnode: VNode,
  360. parentComponent: ComponentInternalInstance | null,
  361. parentSuspense: SuspenseBoundary | null,
  362. optimized: boolean
  363. ) => {
  364. const container = parentNode(node)!
  365. const next = hydrateChildren(
  366. nextSibling(node)!,
  367. vnode,
  368. container,
  369. parentComponent,
  370. parentSuspense,
  371. optimized
  372. )
  373. if (next && isComment(next) && next.data === ']') {
  374. return nextSibling((vnode.anchor = next))
  375. } else {
  376. // fragment didn't hydrate successfully, since we didn't get a end anchor
  377. // back. This should have led to node/children mismatch warnings.
  378. hasMismatch = true
  379. // since the anchor is missing, we need to create one and insert it
  380. insert((vnode.anchor = createComment(`]`)), container, next)
  381. return next
  382. }
  383. }
  384. const handleMismtach = (
  385. node: Node,
  386. vnode: VNode,
  387. parentComponent: ComponentInternalInstance | null,
  388. parentSuspense: SuspenseBoundary | null,
  389. isFragment: boolean
  390. ): Node | null => {
  391. hasMismatch = true
  392. __DEV__ &&
  393. warn(
  394. `Hydration node mismatch:\n- Client vnode:`,
  395. vnode.type,
  396. `\n- Server rendered DOM:`,
  397. node,
  398. node.nodeType === DOMNodeTypes.TEXT
  399. ? `(text)`
  400. : isComment(node) && node.data === '['
  401. ? `(start of fragment)`
  402. : ``
  403. )
  404. vnode.el = null
  405. if (isFragment) {
  406. // remove excessive fragment nodes
  407. const end = locateClosingAsyncAnchor(node)
  408. while (true) {
  409. const next = nextSibling(node)
  410. if (next && next !== end) {
  411. remove(next)
  412. } else {
  413. break
  414. }
  415. }
  416. }
  417. const next = nextSibling(node)
  418. const container = parentNode(node)!
  419. remove(node)
  420. patch(
  421. null,
  422. vnode,
  423. container,
  424. next,
  425. parentComponent,
  426. parentSuspense,
  427. isSVGContainer(container)
  428. )
  429. return next
  430. }
  431. const locateClosingAsyncAnchor = (node: Node | null): Node | null => {
  432. let match = 0
  433. while (node) {
  434. node = nextSibling(node)
  435. if (node && isComment(node)) {
  436. if (node.data === '[') match++
  437. if (node.data === ']') {
  438. if (match === 0) {
  439. return nextSibling(node)
  440. } else {
  441. match--
  442. }
  443. }
  444. }
  445. }
  446. return node
  447. }
  448. return [hydrate, hydrateNode] as const
  449. }