hydration.ts 19 KB

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