hydration.ts 30 KB

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