hydration.ts 17 KB

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