hydration.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  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, TeleportVNode } 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. handleMismatch(
  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 as TeleportVNode,
  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, parentSuspense, 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. if (dirs) {
  235. invokeDirectiveHook(vnode, null, parentComponent, 'created')
  236. }
  237. // props
  238. if (props) {
  239. if (
  240. !optimized ||
  241. (patchFlag & PatchFlags.FULL_PROPS ||
  242. patchFlag & PatchFlags.HYDRATE_EVENTS)
  243. ) {
  244. for (const key in props) {
  245. if (!isReservedProp(key) && isOn(key)) {
  246. patchProp(el, key, null, props[key])
  247. }
  248. }
  249. } else if (props.onClick) {
  250. // Fast path for click listeners (which is most often) to avoid
  251. // iterating through props.
  252. patchProp(el, 'onClick', null, props.onClick)
  253. }
  254. }
  255. // vnode / directive hooks
  256. let vnodeHooks: VNodeHook | null | undefined
  257. if ((vnodeHooks = props && props.onVnodeBeforeMount)) {
  258. invokeVNodeHook(vnodeHooks, parentComponent, vnode)
  259. }
  260. if (dirs) {
  261. invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
  262. }
  263. if ((vnodeHooks = props && props.onVnodeMounted) || dirs) {
  264. queueEffectWithSuspense(() => {
  265. vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode)
  266. dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
  267. }, parentSuspense)
  268. }
  269. // children
  270. if (
  271. shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
  272. // skip if element has innerHTML / textContent
  273. !(props && (props.innerHTML || props.textContent))
  274. ) {
  275. let next = hydrateChildren(
  276. el.firstChild,
  277. vnode,
  278. el,
  279. parentComponent,
  280. parentSuspense,
  281. optimized
  282. )
  283. let hasWarned = false
  284. while (next) {
  285. hasMismatch = true
  286. if (__DEV__ && !hasWarned) {
  287. warn(
  288. `Hydration children mismatch in <${vnode.type as string}>: ` +
  289. `server rendered element contains more child nodes than client vdom.`
  290. )
  291. hasWarned = true
  292. }
  293. // The SSRed DOM contains more nodes than it should. Remove them.
  294. const cur = next
  295. next = next.nextSibling
  296. remove(cur)
  297. }
  298. } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  299. if (el.textContent !== vnode.children) {
  300. hasMismatch = true
  301. __DEV__ &&
  302. warn(
  303. `Hydration text content mismatch in <${vnode.type as string}>:\n` +
  304. `- Client: ${el.textContent}\n` +
  305. `- Server: ${vnode.children as string}`
  306. )
  307. el.textContent = vnode.children as string
  308. }
  309. }
  310. }
  311. return el.nextSibling
  312. }
  313. const hydrateChildren = (
  314. node: Node | null,
  315. parentVNode: VNode,
  316. container: Element,
  317. parentComponent: ComponentInternalInstance | null,
  318. parentSuspense: SuspenseBoundary | null,
  319. optimized: boolean
  320. ): Node | null => {
  321. optimized = optimized || !!parentVNode.dynamicChildren
  322. const children = parentVNode.children as VNode[]
  323. const l = children.length
  324. let hasWarned = false
  325. for (let i = 0; i < l; i++) {
  326. const vnode = optimized
  327. ? children[i]
  328. : (children[i] = normalizeVNode(children[i]))
  329. if (node) {
  330. node = hydrateNode(
  331. node,
  332. vnode,
  333. parentComponent,
  334. parentSuspense,
  335. optimized
  336. )
  337. } else {
  338. hasMismatch = true
  339. if (__DEV__ && !hasWarned) {
  340. warn(
  341. `Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` +
  342. `server rendered element contains fewer child nodes than client vdom.`
  343. )
  344. hasWarned = true
  345. }
  346. // the SSRed DOM didn't contain enough nodes. Mount the missing ones.
  347. patch(
  348. null,
  349. vnode,
  350. container,
  351. null,
  352. parentComponent,
  353. parentSuspense,
  354. isSVGContainer(container)
  355. )
  356. }
  357. }
  358. return node
  359. }
  360. const hydrateFragment = (
  361. node: Comment,
  362. vnode: VNode,
  363. parentComponent: ComponentInternalInstance | null,
  364. parentSuspense: SuspenseBoundary | null,
  365. optimized: boolean
  366. ) => {
  367. const container = parentNode(node)!
  368. const next = hydrateChildren(
  369. nextSibling(node)!,
  370. vnode,
  371. container,
  372. parentComponent,
  373. parentSuspense,
  374. optimized
  375. )
  376. if (next && isComment(next) && next.data === ']') {
  377. return nextSibling((vnode.anchor = next))
  378. } else {
  379. // fragment didn't hydrate successfully, since we didn't get a end anchor
  380. // back. This should have led to node/children mismatch warnings.
  381. hasMismatch = true
  382. // since the anchor is missing, we need to create one and insert it
  383. insert((vnode.anchor = createComment(`]`)), container, next)
  384. return next
  385. }
  386. }
  387. const handleMismatch = (
  388. node: Node,
  389. vnode: VNode,
  390. parentComponent: ComponentInternalInstance | null,
  391. parentSuspense: SuspenseBoundary | null,
  392. isFragment: boolean
  393. ): Node | null => {
  394. hasMismatch = true
  395. __DEV__ &&
  396. warn(
  397. `Hydration node mismatch:\n- Client vnode:`,
  398. vnode.type,
  399. `\n- Server rendered DOM:`,
  400. node,
  401. node.nodeType === DOMNodeTypes.TEXT
  402. ? `(text)`
  403. : isComment(node) && node.data === '['
  404. ? `(start of fragment)`
  405. : ``
  406. )
  407. vnode.el = null
  408. if (isFragment) {
  409. // remove excessive fragment nodes
  410. const end = locateClosingAsyncAnchor(node)
  411. while (true) {
  412. const next = nextSibling(node)
  413. if (next && next !== end) {
  414. remove(next)
  415. } else {
  416. break
  417. }
  418. }
  419. }
  420. const next = nextSibling(node)
  421. const container = parentNode(node)!
  422. remove(node)
  423. patch(
  424. null,
  425. vnode,
  426. container,
  427. next,
  428. parentComponent,
  429. parentSuspense,
  430. isSVGContainer(container)
  431. )
  432. return next
  433. }
  434. const locateClosingAsyncAnchor = (node: Node | null): Node | null => {
  435. let match = 0
  436. while (node) {
  437. node = nextSibling(node)
  438. if (node && isComment(node)) {
  439. if (node.data === '[') match++
  440. if (node.data === ']') {
  441. if (match === 0) {
  442. return nextSibling(node)
  443. } else {
  444. match--
  445. }
  446. }
  447. }
  448. }
  449. return node
  450. }
  451. return [hydrate, hydrateNode] as const
  452. }