hydration.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909
  1. import {
  2. Comment,
  3. Fragment,
  4. Static,
  5. Text,
  6. type VNode,
  7. type VNodeHook,
  8. createTextVNode,
  9. createVNode,
  10. invokeVNodeHook,
  11. normalizeVNode,
  12. } from './vnode'
  13. import { flushPostFlushCbs } from './scheduler'
  14. import type { ComponentInternalInstance } from './component'
  15. import { invokeDirectiveHook } from './directives'
  16. import { warn } from './warning'
  17. import {
  18. PatchFlags,
  19. ShapeFlags,
  20. def,
  21. includeBooleanAttr,
  22. isBooleanAttr,
  23. isKnownHtmlAttr,
  24. isKnownSvgAttr,
  25. isOn,
  26. isRenderableAttrValue,
  27. isReservedProp,
  28. isString,
  29. normalizeClass,
  30. normalizeStyle,
  31. stringifyStyle,
  32. } from '@vue/shared'
  33. import { type RendererInternals, needTransition } from './renderer'
  34. import { setRef } from './rendererTemplateRef'
  35. import {
  36. type SuspenseBoundary,
  37. type SuspenseImpl,
  38. queueEffectWithSuspense,
  39. } from './components/Suspense'
  40. import type { TeleportImpl, TeleportVNode } from './components/Teleport'
  41. import { isAsyncWrapper } from './apiAsyncComponent'
  42. import { isReactive } from '@vue/reactivity'
  43. export type RootHydrateFunction = (
  44. vnode: VNode<Node, Element>,
  45. container: (Element | ShadowRoot) & { _vnode?: VNode },
  46. ) => void
  47. enum DOMNodeTypes {
  48. ELEMENT = 1,
  49. TEXT = 3,
  50. COMMENT = 8,
  51. }
  52. let hasLoggedMismatchError = false
  53. const logMismatchError = () => {
  54. if (__TEST__ || hasLoggedMismatchError) {
  55. return
  56. }
  57. // this error should show up in production
  58. console.error('Hydration completed but contains mismatches.')
  59. hasLoggedMismatchError = true
  60. }
  61. const isSVGContainer = (container: Element) =>
  62. container.namespaceURI!.includes('svg') &&
  63. container.tagName !== 'foreignObject'
  64. const isMathMLContainer = (container: Element) =>
  65. container.namespaceURI!.includes('MathML')
  66. const getContainerType = (container: Element): 'svg' | 'mathml' | undefined => {
  67. if (isSVGContainer(container)) return 'svg'
  68. if (isMathMLContainer(container)) return 'mathml'
  69. return undefined
  70. }
  71. const isComment = (node: Node): node is Comment =>
  72. node.nodeType === DOMNodeTypes.COMMENT
  73. // Note: hydration is DOM-specific
  74. // But we have to place it in core due to tight coupling with core - splitting
  75. // it out creates a ton of unnecessary complexity.
  76. // Hydration also depends on some renderer internal logic which needs to be
  77. // passed in via arguments.
  78. export function createHydrationFunctions(
  79. rendererInternals: RendererInternals<Node, Element>,
  80. ) {
  81. const {
  82. mt: mountComponent,
  83. p: patch,
  84. o: {
  85. patchProp,
  86. createText,
  87. nextSibling,
  88. parentNode,
  89. remove,
  90. insert,
  91. createComment,
  92. },
  93. } = rendererInternals
  94. const hydrate: RootHydrateFunction = (vnode, container) => {
  95. if (!container.hasChildNodes()) {
  96. ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
  97. warn(
  98. `Attempting to hydrate existing markup but container is empty. ` +
  99. `Performing full mount instead.`,
  100. )
  101. patch(null, vnode, container)
  102. flushPostFlushCbs()
  103. container._vnode = vnode
  104. return
  105. }
  106. hydrateNode(container.firstChild!, vnode, null, null, null)
  107. flushPostFlushCbs()
  108. container._vnode = vnode
  109. }
  110. const hydrateNode = (
  111. node: Node,
  112. vnode: VNode,
  113. parentComponent: ComponentInternalInstance | null,
  114. parentSuspense: SuspenseBoundary | null,
  115. slotScopeIds: string[] | null,
  116. optimized = false,
  117. ): Node | null => {
  118. optimized = optimized || !!vnode.dynamicChildren
  119. const isFragmentStart = isComment(node) && node.data === '['
  120. const onMismatch = () =>
  121. handleMismatch(
  122. node,
  123. vnode,
  124. parentComponent,
  125. parentSuspense,
  126. slotScopeIds,
  127. isFragmentStart,
  128. )
  129. const { type, ref, shapeFlag, patchFlag } = vnode
  130. let domType = node.nodeType
  131. vnode.el = node
  132. if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
  133. def(node, '__vnode', vnode, true)
  134. def(node, '__vueParentComponent', parentComponent, true)
  135. }
  136. if (patchFlag === PatchFlags.BAIL) {
  137. optimized = false
  138. vnode.dynamicChildren = null
  139. }
  140. let nextNode: Node | null = null
  141. switch (type) {
  142. case Text:
  143. if (domType !== DOMNodeTypes.TEXT) {
  144. // #5728 empty text node inside a slot can cause hydration failure
  145. // because the server rendered HTML won't contain a text node
  146. if (vnode.children === '') {
  147. insert((vnode.el = createText('')), parentNode(node)!, node)
  148. nextNode = node
  149. } else {
  150. nextNode = onMismatch()
  151. }
  152. } else {
  153. if ((node as Text).data !== vnode.children) {
  154. ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
  155. warn(
  156. `Hydration text mismatch in`,
  157. node.parentNode,
  158. `\n - rendered on server: ${JSON.stringify(
  159. (node as Text).data,
  160. )}` +
  161. `\n - expected on client: ${JSON.stringify(vnode.children)}`,
  162. )
  163. logMismatchError()
  164. ;(node as Text).data = vnode.children as string
  165. }
  166. nextNode = nextSibling(node)
  167. }
  168. break
  169. case Comment:
  170. if (isTemplateNode(node)) {
  171. nextNode = nextSibling(node)
  172. // wrapped <transition appear>
  173. // replace <template> node with inner child
  174. replaceNode(
  175. (vnode.el = node.content.firstChild!),
  176. node,
  177. parentComponent,
  178. )
  179. } else if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) {
  180. nextNode = onMismatch()
  181. } else {
  182. nextNode = nextSibling(node)
  183. }
  184. break
  185. case Static:
  186. if (isFragmentStart) {
  187. // entire template is static but SSRed as a fragment
  188. node = nextSibling(node)!
  189. domType = node.nodeType
  190. }
  191. if (domType === DOMNodeTypes.ELEMENT || domType === DOMNodeTypes.TEXT) {
  192. // determine anchor, adopt content
  193. nextNode = node
  194. // if the static vnode has its content stripped during build,
  195. // adopt it from the server-rendered HTML.
  196. const needToAdoptContent = !(vnode.children as string).length
  197. for (let i = 0; i < vnode.staticCount!; i++) {
  198. if (needToAdoptContent)
  199. vnode.children +=
  200. nextNode.nodeType === DOMNodeTypes.ELEMENT
  201. ? (nextNode as Element).outerHTML
  202. : (nextNode as Text).data
  203. if (i === vnode.staticCount! - 1) {
  204. vnode.anchor = nextNode
  205. }
  206. nextNode = nextSibling(nextNode)!
  207. }
  208. return isFragmentStart ? nextSibling(nextNode) : nextNode
  209. } else {
  210. onMismatch()
  211. }
  212. break
  213. case Fragment:
  214. if (!isFragmentStart) {
  215. nextNode = onMismatch()
  216. } else {
  217. nextNode = hydrateFragment(
  218. node as Comment,
  219. vnode,
  220. parentComponent,
  221. parentSuspense,
  222. slotScopeIds,
  223. optimized,
  224. )
  225. }
  226. break
  227. default:
  228. if (shapeFlag & ShapeFlags.ELEMENT) {
  229. if (
  230. (domType !== DOMNodeTypes.ELEMENT ||
  231. (vnode.type as string).toLowerCase() !==
  232. (node as Element).tagName.toLowerCase()) &&
  233. !isTemplateNode(node)
  234. ) {
  235. nextNode = onMismatch()
  236. } else {
  237. nextNode = hydrateElement(
  238. node as Element,
  239. vnode,
  240. parentComponent,
  241. parentSuspense,
  242. slotScopeIds,
  243. optimized,
  244. )
  245. }
  246. } else if (shapeFlag & ShapeFlags.COMPONENT) {
  247. // when setting up the render effect, if the initial vnode already
  248. // has .el set, the component will perform hydration instead of mount
  249. // on its sub-tree.
  250. vnode.slotScopeIds = slotScopeIds
  251. const container = parentNode(node)!
  252. // Locate the next node.
  253. if (isFragmentStart) {
  254. // If it's a fragment: since components may be async, we cannot rely
  255. // on component's rendered output to determine the end of the
  256. // fragment. Instead, we do a lookahead to find the end anchor node.
  257. nextNode = locateClosingAnchor(node)
  258. } else if (isComment(node) && node.data === 'teleport start') {
  259. // #4293 #6152
  260. // If a teleport is at component root, look ahead for teleport end.
  261. nextNode = locateClosingAnchor(node, node.data, 'teleport end')
  262. } else {
  263. nextNode = nextSibling(node)
  264. }
  265. mountComponent(
  266. vnode,
  267. container,
  268. null,
  269. parentComponent,
  270. parentSuspense,
  271. getContainerType(container),
  272. optimized,
  273. )
  274. // #3787
  275. // if component is async, it may get moved / unmounted before its
  276. // inner component is loaded, so we need to give it a placeholder
  277. // vnode that matches its adopted DOM.
  278. if (isAsyncWrapper(vnode)) {
  279. let subTree
  280. if (isFragmentStart) {
  281. subTree = createVNode(Fragment)
  282. subTree.anchor = nextNode
  283. ? nextNode.previousSibling
  284. : container.lastChild
  285. } else {
  286. subTree =
  287. node.nodeType === 3 ? createTextVNode('') : createVNode('div')
  288. }
  289. subTree.el = node
  290. vnode.component!.subTree = subTree
  291. }
  292. } else if (shapeFlag & ShapeFlags.TELEPORT) {
  293. if (domType !== DOMNodeTypes.COMMENT) {
  294. nextNode = onMismatch()
  295. } else {
  296. nextNode = (vnode.type as typeof TeleportImpl).hydrate(
  297. node,
  298. vnode as TeleportVNode,
  299. parentComponent,
  300. parentSuspense,
  301. slotScopeIds,
  302. optimized,
  303. rendererInternals,
  304. hydrateChildren,
  305. )
  306. }
  307. } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
  308. nextNode = (vnode.type as typeof SuspenseImpl).hydrate(
  309. node,
  310. vnode,
  311. parentComponent,
  312. parentSuspense,
  313. getContainerType(parentNode(node)!),
  314. slotScopeIds,
  315. optimized,
  316. rendererInternals,
  317. hydrateNode,
  318. )
  319. } else if (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) {
  320. warn('Invalid HostVNode type:', type, `(${typeof type})`)
  321. }
  322. }
  323. if (ref != null) {
  324. setRef(ref, null, parentSuspense, vnode)
  325. }
  326. return nextNode
  327. }
  328. const hydrateElement = (
  329. el: Element,
  330. vnode: VNode,
  331. parentComponent: ComponentInternalInstance | null,
  332. parentSuspense: SuspenseBoundary | null,
  333. slotScopeIds: string[] | null,
  334. optimized: boolean,
  335. ) => {
  336. optimized = optimized || !!vnode.dynamicChildren
  337. const { type, props, patchFlag, shapeFlag, dirs, transition } = vnode
  338. // #4006 for form elements with non-string v-model value bindings
  339. // e.g. <option :value="obj">, <input type="checkbox" :true-value="1">
  340. // #7476 <input indeterminate>
  341. const forcePatch = type === 'input' || type === 'option'
  342. // skip props & children if this is hoisted static nodes
  343. // #5405 in dev, always hydrate children for HMR
  344. if (__DEV__ || forcePatch || patchFlag !== PatchFlags.HOISTED) {
  345. if (dirs) {
  346. invokeDirectiveHook(vnode, null, parentComponent, 'created')
  347. }
  348. // handle appear transition
  349. let needCallTransitionHooks = false
  350. if (isTemplateNode(el)) {
  351. needCallTransitionHooks =
  352. needTransition(parentSuspense, transition) &&
  353. parentComponent &&
  354. parentComponent.vnode.props &&
  355. parentComponent.vnode.props.appear
  356. const content = (el as HTMLTemplateElement).content
  357. .firstChild as Element
  358. if (needCallTransitionHooks) {
  359. transition!.beforeEnter(content)
  360. }
  361. // replace <template> node with inner children
  362. replaceNode(content, el, parentComponent)
  363. vnode.el = el = content
  364. }
  365. // children
  366. if (
  367. shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
  368. // skip if element has innerHTML / textContent
  369. !(props && (props.innerHTML || props.textContent))
  370. ) {
  371. let next = hydrateChildren(
  372. el.firstChild,
  373. vnode,
  374. el,
  375. parentComponent,
  376. parentSuspense,
  377. slotScopeIds,
  378. optimized,
  379. )
  380. let hasWarned = false
  381. while (next) {
  382. if (
  383. (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
  384. !hasWarned
  385. ) {
  386. warn(
  387. `Hydration children mismatch on`,
  388. el,
  389. `\nServer rendered element contains more child nodes than client vdom.`,
  390. )
  391. hasWarned = true
  392. }
  393. logMismatchError()
  394. // The SSRed DOM contains more nodes than it should. Remove them.
  395. const cur = next
  396. next = next.nextSibling
  397. remove(cur)
  398. }
  399. } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  400. if (el.textContent !== vnode.children) {
  401. ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
  402. warn(
  403. `Hydration text content mismatch on`,
  404. el,
  405. `\n - rendered on server: ${el.textContent}` +
  406. `\n - expected on client: ${vnode.children as string}`,
  407. )
  408. logMismatchError()
  409. el.textContent = vnode.children as string
  410. }
  411. }
  412. // props
  413. if (props) {
  414. if (
  415. __DEV__ ||
  416. __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ ||
  417. forcePatch ||
  418. !optimized ||
  419. patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.NEED_HYDRATION)
  420. ) {
  421. for (const key in props) {
  422. // check hydration mismatch
  423. if (
  424. (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
  425. // #11189 skip if this node has directives that have created hooks
  426. // as it could have mutated the DOM in any possible way
  427. !(dirs && dirs.some(d => d.dir.created)) &&
  428. propHasMismatch(el, key, props[key], vnode, parentComponent)
  429. ) {
  430. logMismatchError()
  431. }
  432. if (
  433. (forcePatch &&
  434. (key.endsWith('value') || key === 'indeterminate')) ||
  435. (isOn(key) && !isReservedProp(key)) ||
  436. // force hydrate v-bind with .prop modifiers
  437. key[0] === '.'
  438. ) {
  439. patchProp(
  440. el,
  441. key,
  442. null,
  443. props[key],
  444. undefined,
  445. undefined,
  446. parentComponent,
  447. )
  448. }
  449. }
  450. } else if (props.onClick) {
  451. // Fast path for click listeners (which is most often) to avoid
  452. // iterating through props.
  453. patchProp(
  454. el,
  455. 'onClick',
  456. null,
  457. props.onClick,
  458. undefined,
  459. undefined,
  460. parentComponent,
  461. )
  462. } else if (patchFlag & PatchFlags.STYLE && isReactive(props.style)) {
  463. // #11372: object style values are iterated during patch instead of
  464. // render/normalization phase, but style patch is skipped during
  465. // hydration, so we need to force iterate the object to track deps
  466. for (const key in props.style) props.style[key]
  467. }
  468. }
  469. // vnode / directive hooks
  470. let vnodeHooks: VNodeHook | null | undefined
  471. if ((vnodeHooks = props && props.onVnodeBeforeMount)) {
  472. invokeVNodeHook(vnodeHooks, parentComponent, vnode)
  473. }
  474. if (dirs) {
  475. invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
  476. }
  477. if (
  478. (vnodeHooks = props && props.onVnodeMounted) ||
  479. dirs ||
  480. needCallTransitionHooks
  481. ) {
  482. queueEffectWithSuspense(() => {
  483. vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode)
  484. needCallTransitionHooks && transition!.enter(el)
  485. dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
  486. }, parentSuspense)
  487. }
  488. }
  489. return el.nextSibling
  490. }
  491. const hydrateChildren = (
  492. node: Node | null,
  493. parentVNode: VNode,
  494. container: Element,
  495. parentComponent: ComponentInternalInstance | null,
  496. parentSuspense: SuspenseBoundary | null,
  497. slotScopeIds: string[] | null,
  498. optimized: boolean,
  499. ): Node | null => {
  500. optimized = optimized || !!parentVNode.dynamicChildren
  501. const children = parentVNode.children as VNode[]
  502. const l = children.length
  503. let hasWarned = false
  504. for (let i = 0; i < l; i++) {
  505. const vnode = optimized
  506. ? children[i]
  507. : (children[i] = normalizeVNode(children[i]))
  508. const isText = vnode.type === Text
  509. if (node) {
  510. if (isText && !optimized) {
  511. // #7285 possible consecutive text vnodes from manual render fns or
  512. // JSX-compiled fns, but on the client the browser parses only 1 text
  513. // node.
  514. // look ahead for next possible text vnode
  515. let next = children[i + 1]
  516. if (next && (next = normalizeVNode(next)).type === Text) {
  517. // create an extra TextNode on the client for the next vnode to
  518. // adopt
  519. insert(
  520. createText(
  521. (node as Text).data.slice((vnode.children as string).length),
  522. ),
  523. container,
  524. nextSibling(node),
  525. )
  526. ;(node as Text).data = vnode.children as string
  527. }
  528. }
  529. node = hydrateNode(
  530. node,
  531. vnode,
  532. parentComponent,
  533. parentSuspense,
  534. slotScopeIds,
  535. optimized,
  536. )
  537. } else if (isText && !vnode.children) {
  538. // #7215 create a TextNode for empty text node
  539. // because server rendered HTML won't contain a text node
  540. insert((vnode.el = createText('')), container)
  541. } else {
  542. if (
  543. (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
  544. !hasWarned
  545. ) {
  546. warn(
  547. `Hydration children mismatch on`,
  548. container,
  549. `\nServer rendered element contains fewer child nodes than client vdom.`,
  550. )
  551. hasWarned = true
  552. }
  553. logMismatchError()
  554. // the SSRed DOM didn't contain enough nodes. Mount the missing ones.
  555. patch(
  556. null,
  557. vnode,
  558. container,
  559. null,
  560. parentComponent,
  561. parentSuspense,
  562. getContainerType(container),
  563. slotScopeIds,
  564. )
  565. }
  566. }
  567. return node
  568. }
  569. const hydrateFragment = (
  570. node: Comment,
  571. vnode: VNode,
  572. parentComponent: ComponentInternalInstance | null,
  573. parentSuspense: SuspenseBoundary | null,
  574. slotScopeIds: string[] | null,
  575. optimized: boolean,
  576. ) => {
  577. const { slotScopeIds: fragmentSlotScopeIds } = vnode
  578. if (fragmentSlotScopeIds) {
  579. slotScopeIds = slotScopeIds
  580. ? slotScopeIds.concat(fragmentSlotScopeIds)
  581. : fragmentSlotScopeIds
  582. }
  583. const container = parentNode(node)!
  584. const next = hydrateChildren(
  585. nextSibling(node)!,
  586. vnode,
  587. container,
  588. parentComponent,
  589. parentSuspense,
  590. slotScopeIds,
  591. optimized,
  592. )
  593. if (next && isComment(next) && next.data === ']') {
  594. return nextSibling((vnode.anchor = next))
  595. } else {
  596. // fragment didn't hydrate successfully, since we didn't get a end anchor
  597. // back. This should have led to node/children mismatch warnings.
  598. logMismatchError()
  599. // since the anchor is missing, we need to create one and insert it
  600. insert((vnode.anchor = createComment(`]`)), container, next)
  601. return next
  602. }
  603. }
  604. const handleMismatch = (
  605. node: Node,
  606. vnode: VNode,
  607. parentComponent: ComponentInternalInstance | null,
  608. parentSuspense: SuspenseBoundary | null,
  609. slotScopeIds: string[] | null,
  610. isFragment: boolean,
  611. ): Node | null => {
  612. ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
  613. warn(
  614. `Hydration node mismatch:\n- rendered on server:`,
  615. node,
  616. node.nodeType === DOMNodeTypes.TEXT
  617. ? `(text)`
  618. : isComment(node) && node.data === '['
  619. ? `(start of fragment)`
  620. : ``,
  621. `\n- expected on client:`,
  622. vnode.type,
  623. )
  624. logMismatchError()
  625. vnode.el = null
  626. if (isFragment) {
  627. // remove excessive fragment nodes
  628. const end = locateClosingAnchor(node)
  629. while (true) {
  630. const next = nextSibling(node)
  631. if (next && next !== end) {
  632. remove(next)
  633. } else {
  634. break
  635. }
  636. }
  637. }
  638. const next = nextSibling(node)
  639. const container = parentNode(node)!
  640. remove(node)
  641. patch(
  642. null,
  643. vnode,
  644. container,
  645. next,
  646. parentComponent,
  647. parentSuspense,
  648. getContainerType(container),
  649. slotScopeIds,
  650. )
  651. return next
  652. }
  653. // looks ahead for a start and closing comment node
  654. const locateClosingAnchor = (
  655. node: Node | null,
  656. open = '[',
  657. close = ']',
  658. ): Node | null => {
  659. let match = 0
  660. while (node) {
  661. node = nextSibling(node)
  662. if (node && isComment(node)) {
  663. if (node.data === open) match++
  664. if (node.data === close) {
  665. if (match === 0) {
  666. return nextSibling(node)
  667. } else {
  668. match--
  669. }
  670. }
  671. }
  672. }
  673. return node
  674. }
  675. const replaceNode = (
  676. newNode: Node,
  677. oldNode: Node,
  678. parentComponent: ComponentInternalInstance | null,
  679. ): void => {
  680. // replace node
  681. const parentNode = oldNode.parentNode
  682. if (parentNode) {
  683. parentNode.replaceChild(newNode, oldNode)
  684. }
  685. // update vnode
  686. let parent = parentComponent
  687. while (parent) {
  688. if (parent.vnode.el === oldNode) {
  689. parent.vnode.el = parent.subTree.el = newNode
  690. }
  691. parent = parent.parent
  692. }
  693. }
  694. const isTemplateNode = (node: Node): node is HTMLTemplateElement => {
  695. return (
  696. node.nodeType === DOMNodeTypes.ELEMENT &&
  697. (node as Element).tagName.toLowerCase() === 'template'
  698. )
  699. }
  700. return [hydrate, hydrateNode] as const
  701. }
  702. /**
  703. * Dev only
  704. */
  705. function propHasMismatch(
  706. el: Element,
  707. key: string,
  708. clientValue: any,
  709. vnode: VNode,
  710. instance: ComponentInternalInstance | null,
  711. ): boolean {
  712. let mismatchType: string | undefined
  713. let mismatchKey: string | undefined
  714. let actual: string | boolean | null | undefined
  715. let expected: string | boolean | null | undefined
  716. if (key === 'class') {
  717. // classes might be in different order, but that doesn't affect cascade
  718. // so we just need to check if the class lists contain the same classes.
  719. actual = el.getAttribute('class')
  720. expected = normalizeClass(clientValue)
  721. if (!isSetEqual(toClassSet(actual || ''), toClassSet(expected))) {
  722. mismatchType = mismatchKey = `class`
  723. }
  724. } else if (key === 'style') {
  725. // style might be in different order, but that doesn't affect cascade
  726. actual = el.getAttribute('style') || ''
  727. expected = isString(clientValue)
  728. ? clientValue
  729. : stringifyStyle(normalizeStyle(clientValue))
  730. const actualMap = toStyleMap(actual)
  731. const expectedMap = toStyleMap(expected)
  732. // If `v-show=false`, `display: 'none'` should be added to expected
  733. if (vnode.dirs) {
  734. for (const { dir, value } of vnode.dirs) {
  735. // @ts-expect-error only vShow has this internal name
  736. if (dir.name === 'show' && !value) {
  737. expectedMap.set('display', 'none')
  738. }
  739. }
  740. }
  741. if (instance) {
  742. resolveCssVars(instance, vnode, expectedMap)
  743. }
  744. if (!isMapEqual(actualMap, expectedMap)) {
  745. mismatchType = mismatchKey = 'style'
  746. }
  747. } else if (
  748. (el instanceof SVGElement && isKnownSvgAttr(key)) ||
  749. (el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key)))
  750. ) {
  751. if (isBooleanAttr(key)) {
  752. actual = el.hasAttribute(key)
  753. expected = includeBooleanAttr(clientValue)
  754. } else if (clientValue == null) {
  755. actual = el.hasAttribute(key)
  756. expected = false
  757. } else {
  758. if (el.hasAttribute(key)) {
  759. actual = el.getAttribute(key)
  760. } else if (key === 'value' && el.tagName === 'TEXTAREA') {
  761. // #10000 textarea.value can't be retrieved by `hasAttribute`
  762. actual = (el as HTMLTextAreaElement).value
  763. } else {
  764. actual = false
  765. }
  766. expected = isRenderableAttrValue(clientValue)
  767. ? String(clientValue)
  768. : false
  769. }
  770. if (actual !== expected) {
  771. mismatchType = `attribute`
  772. mismatchKey = key
  773. }
  774. }
  775. if (mismatchType) {
  776. const format = (v: any) =>
  777. v === false ? `(not rendered)` : `${mismatchKey}="${v}"`
  778. const preSegment = `Hydration ${mismatchType} mismatch on`
  779. const postSegment =
  780. `\n - rendered on server: ${format(actual)}` +
  781. `\n - expected on client: ${format(expected)}` +
  782. `\n Note: this mismatch is check-only. The DOM will not be rectified ` +
  783. `in production due to performance overhead.` +
  784. `\n You should fix the source of the mismatch.`
  785. if (__TEST__) {
  786. // during tests, log the full message in one single string for easier
  787. // debugging.
  788. warn(`${preSegment} ${el.tagName}${postSegment}`)
  789. } else {
  790. warn(preSegment, el, postSegment)
  791. }
  792. return true
  793. }
  794. return false
  795. }
  796. function toClassSet(str: string): Set<string> {
  797. return new Set(str.trim().split(/\s+/))
  798. }
  799. function isSetEqual(a: Set<string>, b: Set<string>): boolean {
  800. if (a.size !== b.size) {
  801. return false
  802. }
  803. for (const s of a) {
  804. if (!b.has(s)) {
  805. return false
  806. }
  807. }
  808. return true
  809. }
  810. function toStyleMap(str: string): Map<string, string> {
  811. const styleMap: Map<string, string> = new Map()
  812. for (const item of str.split(';')) {
  813. let [key, value] = item.split(':')
  814. key = key.trim()
  815. value = value && value.trim()
  816. if (key && value) {
  817. styleMap.set(key, value)
  818. }
  819. }
  820. return styleMap
  821. }
  822. function isMapEqual(a: Map<string, string>, b: Map<string, string>): boolean {
  823. if (a.size !== b.size) {
  824. return false
  825. }
  826. for (const [key, value] of a) {
  827. if (value !== b.get(key)) {
  828. return false
  829. }
  830. }
  831. return true
  832. }
  833. function resolveCssVars(
  834. instance: ComponentInternalInstance,
  835. vnode: VNode,
  836. expectedMap: Map<string, string>,
  837. ) {
  838. const root = instance.subTree
  839. if (
  840. instance.getCssVars &&
  841. (vnode === root ||
  842. (root &&
  843. root.type === Fragment &&
  844. (root.children as VNode[]).includes(vnode)))
  845. ) {
  846. const cssVars = instance.getCssVars()
  847. for (const key in cssVars) {
  848. expectedMap.set(`--${key}`, String(cssVars[key]))
  849. }
  850. }
  851. if (vnode === root && instance.parent) {
  852. resolveCssVars(instance.parent, instance.vnode, expectedMap)
  853. }
  854. }