hydration.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  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. const 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 (domType !== DOMNodeTypes.ELEMENT) {
  143. nextNode = onMismatch()
  144. } else {
  145. // determine anchor, adopt content
  146. nextNode = node
  147. // if the static vnode has its content stripped during build,
  148. // adopt it from the server-rendered HTML.
  149. const needToAdoptContent = !(vnode.children as string).length
  150. for (let i = 0; i < vnode.staticCount!; i++) {
  151. if (needToAdoptContent)
  152. vnode.children += (nextNode as Element).outerHTML
  153. if (i === vnode.staticCount! - 1) {
  154. vnode.anchor = nextNode
  155. }
  156. nextNode = nextSibling(nextNode)!
  157. }
  158. return nextNode
  159. }
  160. break
  161. case Fragment:
  162. if (!isFragmentStart) {
  163. nextNode = onMismatch()
  164. } else {
  165. nextNode = hydrateFragment(
  166. node as Comment,
  167. vnode,
  168. parentComponent,
  169. parentSuspense,
  170. slotScopeIds,
  171. optimized
  172. )
  173. }
  174. break
  175. default:
  176. if (shapeFlag & ShapeFlags.ELEMENT) {
  177. if (
  178. domType !== DOMNodeTypes.ELEMENT ||
  179. (vnode.type as string).toLowerCase() !==
  180. (node as Element).tagName.toLowerCase()
  181. ) {
  182. nextNode = onMismatch()
  183. } else {
  184. nextNode = hydrateElement(
  185. node as Element,
  186. vnode,
  187. parentComponent,
  188. parentSuspense,
  189. slotScopeIds,
  190. optimized
  191. )
  192. }
  193. } else if (shapeFlag & ShapeFlags.COMPONENT) {
  194. // when setting up the render effect, if the initial vnode already
  195. // has .el set, the component will perform hydration instead of mount
  196. // on its sub-tree.
  197. vnode.slotScopeIds = slotScopeIds
  198. const container = parentNode(node)!
  199. mountComponent(
  200. vnode,
  201. container,
  202. null,
  203. parentComponent,
  204. parentSuspense,
  205. isSVGContainer(container),
  206. optimized
  207. )
  208. // component may be async, so in the case of fragments we cannot rely
  209. // on component's rendered output to determine the end of the fragment
  210. // instead, we do a lookahead to find the end anchor node.
  211. nextNode = isFragmentStart
  212. ? locateClosingAsyncAnchor(node)
  213. : nextSibling(node)
  214. // #4293 teleport as component root
  215. if (
  216. nextNode &&
  217. isComment(nextNode) &&
  218. nextNode.data === 'teleport end'
  219. ) {
  220. nextNode = nextSibling(nextNode)
  221. }
  222. // #3787
  223. // if component is async, it may get moved / unmounted before its
  224. // inner component is loaded, so we need to give it a placeholder
  225. // vnode that matches its adopted DOM.
  226. if (isAsyncWrapper(vnode)) {
  227. let subTree
  228. if (isFragmentStart) {
  229. subTree = createVNode(Fragment)
  230. subTree.anchor = nextNode
  231. ? nextNode.previousSibling
  232. : container.lastChild
  233. } else {
  234. subTree =
  235. node.nodeType === 3 ? createTextVNode('') : createVNode('div')
  236. }
  237. subTree.el = node
  238. vnode.component!.subTree = subTree
  239. }
  240. } else if (shapeFlag & ShapeFlags.TELEPORT) {
  241. if (domType !== DOMNodeTypes.COMMENT) {
  242. nextNode = onMismatch()
  243. } else {
  244. nextNode = (vnode.type as typeof TeleportImpl).hydrate(
  245. node,
  246. vnode as TeleportVNode,
  247. parentComponent,
  248. parentSuspense,
  249. slotScopeIds,
  250. optimized,
  251. rendererInternals,
  252. hydrateChildren
  253. )
  254. }
  255. } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
  256. nextNode = (vnode.type as typeof SuspenseImpl).hydrate(
  257. node,
  258. vnode,
  259. parentComponent,
  260. parentSuspense,
  261. isSVGContainer(parentNode(node)!),
  262. slotScopeIds,
  263. optimized,
  264. rendererInternals,
  265. hydrateNode
  266. )
  267. } else if (__DEV__) {
  268. warn('Invalid HostVNode type:', type, `(${typeof type})`)
  269. }
  270. }
  271. if (ref != null) {
  272. setRef(ref, null, parentSuspense, vnode)
  273. }
  274. return nextNode
  275. }
  276. const hydrateElement = (
  277. el: Element,
  278. vnode: VNode,
  279. parentComponent: ComponentInternalInstance | null,
  280. parentSuspense: SuspenseBoundary | null,
  281. slotScopeIds: string[] | null,
  282. optimized: boolean
  283. ) => {
  284. optimized = optimized || !!vnode.dynamicChildren
  285. const { type, props, patchFlag, shapeFlag, dirs } = vnode
  286. // #4006 for form elements with non-string v-model value bindings
  287. // e.g. <option :value="obj">, <input type="checkbox" :true-value="1">
  288. const forcePatchValue = (type === 'input' && dirs) || type === 'option'
  289. // skip props & children if this is hoisted static nodes
  290. // #5405 in dev, always hydrate children for HMR
  291. if (__DEV__ || forcePatchValue || patchFlag !== PatchFlags.HOISTED) {
  292. if (dirs) {
  293. invokeDirectiveHook(vnode, null, parentComponent, 'created')
  294. }
  295. // props
  296. if (props) {
  297. if (
  298. forcePatchValue ||
  299. !optimized ||
  300. patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.HYDRATE_EVENTS)
  301. ) {
  302. for (const key in props) {
  303. if (
  304. (forcePatchValue && key.endsWith('value')) ||
  305. (isOn(key) && !isReservedProp(key))
  306. ) {
  307. patchProp(
  308. el,
  309. key,
  310. null,
  311. props[key],
  312. false,
  313. undefined,
  314. parentComponent
  315. )
  316. }
  317. }
  318. } else if (props.onClick) {
  319. // Fast path for click listeners (which is most often) to avoid
  320. // iterating through props.
  321. patchProp(
  322. el,
  323. 'onClick',
  324. null,
  325. props.onClick,
  326. false,
  327. undefined,
  328. parentComponent
  329. )
  330. }
  331. }
  332. // vnode / directive hooks
  333. let vnodeHooks: VNodeHook | null | undefined
  334. if ((vnodeHooks = props && props.onVnodeBeforeMount)) {
  335. invokeVNodeHook(vnodeHooks, parentComponent, vnode)
  336. }
  337. if (dirs) {
  338. invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
  339. }
  340. if ((vnodeHooks = props && props.onVnodeMounted) || dirs) {
  341. queueEffectWithSuspense(() => {
  342. vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode)
  343. dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
  344. }, parentSuspense)
  345. }
  346. // children
  347. if (
  348. shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
  349. // skip if element has innerHTML / textContent
  350. !(props && (props.innerHTML || props.textContent))
  351. ) {
  352. let next = hydrateChildren(
  353. el.firstChild,
  354. vnode,
  355. el,
  356. parentComponent,
  357. parentSuspense,
  358. slotScopeIds,
  359. optimized
  360. )
  361. let hasWarned = false
  362. while (next) {
  363. hasMismatch = true
  364. if (__DEV__ && !hasWarned) {
  365. warn(
  366. `Hydration children mismatch in <${vnode.type as string}>: ` +
  367. `server rendered element contains more child nodes than client vdom.`
  368. )
  369. hasWarned = true
  370. }
  371. // The SSRed DOM contains more nodes than it should. Remove them.
  372. const cur = next
  373. next = next.nextSibling
  374. remove(cur)
  375. }
  376. } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  377. if (el.textContent !== vnode.children) {
  378. hasMismatch = true
  379. __DEV__ &&
  380. warn(
  381. `Hydration text content mismatch in <${
  382. vnode.type as string
  383. }>:\n` +
  384. `- Client: ${el.textContent}\n` +
  385. `- Server: ${vnode.children as string}`
  386. )
  387. el.textContent = vnode.children as string
  388. }
  389. }
  390. }
  391. return el.nextSibling
  392. }
  393. const hydrateChildren = (
  394. node: Node | null,
  395. parentVNode: VNode,
  396. container: Element,
  397. parentComponent: ComponentInternalInstance | null,
  398. parentSuspense: SuspenseBoundary | null,
  399. slotScopeIds: string[] | null,
  400. optimized: boolean
  401. ): Node | null => {
  402. optimized = optimized || !!parentVNode.dynamicChildren
  403. const children = parentVNode.children as VNode[]
  404. const l = children.length
  405. let hasWarned = false
  406. for (let i = 0; i < l; i++) {
  407. const vnode = optimized
  408. ? children[i]
  409. : (children[i] = normalizeVNode(children[i]))
  410. if (node) {
  411. node = hydrateNode(
  412. node,
  413. vnode,
  414. parentComponent,
  415. parentSuspense,
  416. slotScopeIds,
  417. optimized
  418. )
  419. } else if (vnode.type === Text && !vnode.children) {
  420. continue
  421. } else {
  422. hasMismatch = true
  423. if (__DEV__ && !hasWarned) {
  424. warn(
  425. `Hydration children mismatch in <${container.tagName.toLowerCase()}>: ` +
  426. `server rendered element contains fewer child nodes than client vdom.`
  427. )
  428. hasWarned = true
  429. }
  430. // the SSRed DOM didn't contain enough nodes. Mount the missing ones.
  431. patch(
  432. null,
  433. vnode,
  434. container,
  435. null,
  436. parentComponent,
  437. parentSuspense,
  438. isSVGContainer(container),
  439. slotScopeIds
  440. )
  441. }
  442. }
  443. return node
  444. }
  445. const hydrateFragment = (
  446. node: Comment,
  447. vnode: VNode,
  448. parentComponent: ComponentInternalInstance | null,
  449. parentSuspense: SuspenseBoundary | null,
  450. slotScopeIds: string[] | null,
  451. optimized: boolean
  452. ) => {
  453. const { slotScopeIds: fragmentSlotScopeIds } = vnode
  454. if (fragmentSlotScopeIds) {
  455. slotScopeIds = slotScopeIds
  456. ? slotScopeIds.concat(fragmentSlotScopeIds)
  457. : fragmentSlotScopeIds
  458. }
  459. const container = parentNode(node)!
  460. const next = hydrateChildren(
  461. nextSibling(node)!,
  462. vnode,
  463. container,
  464. parentComponent,
  465. parentSuspense,
  466. slotScopeIds,
  467. optimized
  468. )
  469. if (next && isComment(next) && next.data === ']') {
  470. return nextSibling((vnode.anchor = next))
  471. } else {
  472. // fragment didn't hydrate successfully, since we didn't get a end anchor
  473. // back. This should have led to node/children mismatch warnings.
  474. hasMismatch = true
  475. // since the anchor is missing, we need to create one and insert it
  476. insert((vnode.anchor = createComment(`]`)), container, next)
  477. return next
  478. }
  479. }
  480. const handleMismatch = (
  481. node: Node,
  482. vnode: VNode,
  483. parentComponent: ComponentInternalInstance | null,
  484. parentSuspense: SuspenseBoundary | null,
  485. slotScopeIds: string[] | null,
  486. isFragment: boolean
  487. ): Node | null => {
  488. hasMismatch = true
  489. __DEV__ &&
  490. warn(
  491. `Hydration node mismatch:\n- Client vnode:`,
  492. vnode.type,
  493. `\n- Server rendered DOM:`,
  494. node,
  495. node.nodeType === DOMNodeTypes.TEXT
  496. ? `(text)`
  497. : isComment(node) && node.data === '['
  498. ? `(start of fragment)`
  499. : ``
  500. )
  501. vnode.el = null
  502. if (isFragment) {
  503. // remove excessive fragment nodes
  504. const end = locateClosingAsyncAnchor(node)
  505. while (true) {
  506. const next = nextSibling(node)
  507. if (next && next !== end) {
  508. remove(next)
  509. } else {
  510. break
  511. }
  512. }
  513. }
  514. const next = nextSibling(node)
  515. const container = parentNode(node)!
  516. remove(node)
  517. patch(
  518. null,
  519. vnode,
  520. container,
  521. next,
  522. parentComponent,
  523. parentSuspense,
  524. isSVGContainer(container),
  525. slotScopeIds
  526. )
  527. return next
  528. }
  529. const locateClosingAsyncAnchor = (node: Node | null): Node | null => {
  530. let match = 0
  531. while (node) {
  532. node = nextSibling(node)
  533. if (node && isComment(node)) {
  534. if (node.data === '[') match++
  535. if (node.data === ']') {
  536. if (match === 0) {
  537. return nextSibling(node)
  538. } else {
  539. match--
  540. }
  541. }
  542. }
  543. }
  544. return node
  545. }
  546. return [hydrate, hydrateNode] as const
  547. }