| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909 |
- import {
- Comment,
- Fragment,
- Static,
- Text,
- type VNode,
- type VNodeHook,
- createTextVNode,
- createVNode,
- invokeVNodeHook,
- normalizeVNode,
- } from './vnode'
- import { flushPostFlushCbs } from './scheduler'
- import type { ComponentInternalInstance } from './component'
- import { invokeDirectiveHook } from './directives'
- import { warn } from './warning'
- import {
- PatchFlags,
- ShapeFlags,
- def,
- includeBooleanAttr,
- isBooleanAttr,
- isKnownHtmlAttr,
- isKnownSvgAttr,
- isOn,
- isRenderableAttrValue,
- isReservedProp,
- isString,
- normalizeClass,
- normalizeStyle,
- stringifyStyle,
- } from '@vue/shared'
- import { type RendererInternals, needTransition } from './renderer'
- import { setRef } from './rendererTemplateRef'
- import {
- type SuspenseBoundary,
- type SuspenseImpl,
- queueEffectWithSuspense,
- } from './components/Suspense'
- import type { TeleportImpl, TeleportVNode } from './components/Teleport'
- import { isAsyncWrapper } from './apiAsyncComponent'
- import { isReactive } from '@vue/reactivity'
- export type RootHydrateFunction = (
- vnode: VNode<Node, Element>,
- container: (Element | ShadowRoot) & { _vnode?: VNode },
- ) => void
- enum DOMNodeTypes {
- ELEMENT = 1,
- TEXT = 3,
- COMMENT = 8,
- }
- let hasLoggedMismatchError = false
- const logMismatchError = () => {
- if (__TEST__ || hasLoggedMismatchError) {
- return
- }
- // this error should show up in production
- console.error('Hydration completed but contains mismatches.')
- hasLoggedMismatchError = true
- }
- const isSVGContainer = (container: Element) =>
- container.namespaceURI!.includes('svg') &&
- container.tagName !== 'foreignObject'
- const isMathMLContainer = (container: Element) =>
- container.namespaceURI!.includes('MathML')
- const getContainerType = (container: Element): 'svg' | 'mathml' | undefined => {
- if (isSVGContainer(container)) return 'svg'
- if (isMathMLContainer(container)) return 'mathml'
- return undefined
- }
- const isComment = (node: Node): node is Comment =>
- node.nodeType === DOMNodeTypes.COMMENT
- // Note: hydration is DOM-specific
- // But we have to place it in core due to tight coupling with core - splitting
- // it out creates a ton of unnecessary complexity.
- // Hydration also depends on some renderer internal logic which needs to be
- // passed in via arguments.
- export function createHydrationFunctions(
- rendererInternals: RendererInternals<Node, Element>,
- ) {
- const {
- mt: mountComponent,
- p: patch,
- o: {
- patchProp,
- createText,
- nextSibling,
- parentNode,
- remove,
- insert,
- createComment,
- },
- } = rendererInternals
- const hydrate: RootHydrateFunction = (vnode, container) => {
- if (!container.hasChildNodes()) {
- ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
- warn(
- `Attempting to hydrate existing markup but container is empty. ` +
- `Performing full mount instead.`,
- )
- patch(null, vnode, container)
- flushPostFlushCbs()
- container._vnode = vnode
- return
- }
- hydrateNode(container.firstChild!, vnode, null, null, null)
- flushPostFlushCbs()
- container._vnode = vnode
- }
- const hydrateNode = (
- node: Node,
- vnode: VNode,
- parentComponent: ComponentInternalInstance | null,
- parentSuspense: SuspenseBoundary | null,
- slotScopeIds: string[] | null,
- optimized = false,
- ): Node | null => {
- optimized = optimized || !!vnode.dynamicChildren
- const isFragmentStart = isComment(node) && node.data === '['
- const onMismatch = () =>
- handleMismatch(
- node,
- vnode,
- parentComponent,
- parentSuspense,
- slotScopeIds,
- isFragmentStart,
- )
- const { type, ref, shapeFlag, patchFlag } = vnode
- let domType = node.nodeType
- vnode.el = node
- if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
- def(node, '__vnode', vnode, true)
- def(node, '__vueParentComponent', parentComponent, true)
- }
- if (patchFlag === PatchFlags.BAIL) {
- optimized = false
- vnode.dynamicChildren = null
- }
- let nextNode: Node | null = null
- switch (type) {
- case Text:
- if (domType !== DOMNodeTypes.TEXT) {
- // #5728 empty text node inside a slot can cause hydration failure
- // because the server rendered HTML won't contain a text node
- if (vnode.children === '') {
- insert((vnode.el = createText('')), parentNode(node)!, node)
- nextNode = node
- } else {
- nextNode = onMismatch()
- }
- } else {
- if ((node as Text).data !== vnode.children) {
- ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
- warn(
- `Hydration text mismatch in`,
- node.parentNode,
- `\n - rendered on server: ${JSON.stringify(
- (node as Text).data,
- )}` +
- `\n - expected on client: ${JSON.stringify(vnode.children)}`,
- )
- logMismatchError()
- ;(node as Text).data = vnode.children as string
- }
- nextNode = nextSibling(node)
- }
- break
- case Comment:
- if (isTemplateNode(node)) {
- nextNode = nextSibling(node)
- // wrapped <transition appear>
- // replace <template> node with inner child
- replaceNode(
- (vnode.el = node.content.firstChild!),
- node,
- parentComponent,
- )
- } else if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) {
- nextNode = onMismatch()
- } else {
- nextNode = nextSibling(node)
- }
- break
- case Static:
- if (isFragmentStart) {
- // entire template is static but SSRed as a fragment
- node = nextSibling(node)!
- domType = node.nodeType
- }
- if (domType === DOMNodeTypes.ELEMENT || domType === DOMNodeTypes.TEXT) {
- // determine anchor, adopt content
- nextNode = node
- // if the static vnode has its content stripped during build,
- // adopt it from the server-rendered HTML.
- const needToAdoptContent = !(vnode.children as string).length
- for (let i = 0; i < vnode.staticCount!; i++) {
- if (needToAdoptContent)
- vnode.children +=
- nextNode.nodeType === DOMNodeTypes.ELEMENT
- ? (nextNode as Element).outerHTML
- : (nextNode as Text).data
- if (i === vnode.staticCount! - 1) {
- vnode.anchor = nextNode
- }
- nextNode = nextSibling(nextNode)!
- }
- return isFragmentStart ? nextSibling(nextNode) : nextNode
- } else {
- onMismatch()
- }
- break
- case Fragment:
- if (!isFragmentStart) {
- nextNode = onMismatch()
- } else {
- nextNode = hydrateFragment(
- node as Comment,
- vnode,
- parentComponent,
- parentSuspense,
- slotScopeIds,
- optimized,
- )
- }
- break
- default:
- if (shapeFlag & ShapeFlags.ELEMENT) {
- if (
- (domType !== DOMNodeTypes.ELEMENT ||
- (vnode.type as string).toLowerCase() !==
- (node as Element).tagName.toLowerCase()) &&
- !isTemplateNode(node)
- ) {
- nextNode = onMismatch()
- } else {
- nextNode = hydrateElement(
- node as Element,
- vnode,
- parentComponent,
- parentSuspense,
- slotScopeIds,
- optimized,
- )
- }
- } else if (shapeFlag & ShapeFlags.COMPONENT) {
- // when setting up the render effect, if the initial vnode already
- // has .el set, the component will perform hydration instead of mount
- // on its sub-tree.
- vnode.slotScopeIds = slotScopeIds
- const container = parentNode(node)!
- // Locate the next node.
- if (isFragmentStart) {
- // If it's a fragment: since components may be async, we cannot rely
- // on component's rendered output to determine the end of the
- // fragment. Instead, we do a lookahead to find the end anchor node.
- nextNode = locateClosingAnchor(node)
- } else if (isComment(node) && node.data === 'teleport start') {
- // #4293 #6152
- // If a teleport is at component root, look ahead for teleport end.
- nextNode = locateClosingAnchor(node, node.data, 'teleport end')
- } else {
- nextNode = nextSibling(node)
- }
- mountComponent(
- vnode,
- container,
- null,
- parentComponent,
- parentSuspense,
- getContainerType(container),
- optimized,
- )
- // #3787
- // if component is async, it may get moved / unmounted before its
- // inner component is loaded, so we need to give it a placeholder
- // vnode that matches its adopted DOM.
- if (isAsyncWrapper(vnode)) {
- let subTree
- if (isFragmentStart) {
- subTree = createVNode(Fragment)
- subTree.anchor = nextNode
- ? nextNode.previousSibling
- : container.lastChild
- } else {
- subTree =
- node.nodeType === 3 ? createTextVNode('') : createVNode('div')
- }
- subTree.el = node
- vnode.component!.subTree = subTree
- }
- } else if (shapeFlag & ShapeFlags.TELEPORT) {
- if (domType !== DOMNodeTypes.COMMENT) {
- nextNode = onMismatch()
- } else {
- nextNode = (vnode.type as typeof TeleportImpl).hydrate(
- node,
- vnode as TeleportVNode,
- parentComponent,
- parentSuspense,
- slotScopeIds,
- optimized,
- rendererInternals,
- hydrateChildren,
- )
- }
- } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
- nextNode = (vnode.type as typeof SuspenseImpl).hydrate(
- node,
- vnode,
- parentComponent,
- parentSuspense,
- getContainerType(parentNode(node)!),
- slotScopeIds,
- optimized,
- rendererInternals,
- hydrateNode,
- )
- } else if (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) {
- warn('Invalid HostVNode type:', type, `(${typeof type})`)
- }
- }
- if (ref != null) {
- setRef(ref, null, parentSuspense, vnode)
- }
- return nextNode
- }
- const hydrateElement = (
- el: Element,
- vnode: VNode,
- parentComponent: ComponentInternalInstance | null,
- parentSuspense: SuspenseBoundary | null,
- slotScopeIds: string[] | null,
- optimized: boolean,
- ) => {
- optimized = optimized || !!vnode.dynamicChildren
- const { type, props, patchFlag, shapeFlag, dirs, transition } = vnode
- // #4006 for form elements with non-string v-model value bindings
- // e.g. <option :value="obj">, <input type="checkbox" :true-value="1">
- // #7476 <input indeterminate>
- const forcePatch = type === 'input' || type === 'option'
- // skip props & children if this is hoisted static nodes
- // #5405 in dev, always hydrate children for HMR
- if (__DEV__ || forcePatch || patchFlag !== PatchFlags.HOISTED) {
- if (dirs) {
- invokeDirectiveHook(vnode, null, parentComponent, 'created')
- }
- // handle appear transition
- let needCallTransitionHooks = false
- if (isTemplateNode(el)) {
- needCallTransitionHooks =
- needTransition(parentSuspense, transition) &&
- parentComponent &&
- parentComponent.vnode.props &&
- parentComponent.vnode.props.appear
- const content = (el as HTMLTemplateElement).content
- .firstChild as Element
- if (needCallTransitionHooks) {
- transition!.beforeEnter(content)
- }
- // replace <template> node with inner children
- replaceNode(content, el, parentComponent)
- vnode.el = el = content
- }
- // children
- if (
- shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
- // skip if element has innerHTML / textContent
- !(props && (props.innerHTML || props.textContent))
- ) {
- let next = hydrateChildren(
- el.firstChild,
- vnode,
- el,
- parentComponent,
- parentSuspense,
- slotScopeIds,
- optimized,
- )
- let hasWarned = false
- while (next) {
- if (
- (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
- !hasWarned
- ) {
- warn(
- `Hydration children mismatch on`,
- el,
- `\nServer rendered element contains more child nodes than client vdom.`,
- )
- hasWarned = true
- }
- logMismatchError()
- // The SSRed DOM contains more nodes than it should. Remove them.
- const cur = next
- next = next.nextSibling
- remove(cur)
- }
- } else if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
- if (el.textContent !== vnode.children) {
- ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
- warn(
- `Hydration text content mismatch on`,
- el,
- `\n - rendered on server: ${el.textContent}` +
- `\n - expected on client: ${vnode.children as string}`,
- )
- logMismatchError()
- el.textContent = vnode.children as string
- }
- }
- // props
- if (props) {
- if (
- __DEV__ ||
- __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__ ||
- forcePatch ||
- !optimized ||
- patchFlag & (PatchFlags.FULL_PROPS | PatchFlags.NEED_HYDRATION)
- ) {
- for (const key in props) {
- // check hydration mismatch
- if (
- (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
- // #11189 skip if this node has directives that have created hooks
- // as it could have mutated the DOM in any possible way
- !(dirs && dirs.some(d => d.dir.created)) &&
- propHasMismatch(el, key, props[key], vnode, parentComponent)
- ) {
- logMismatchError()
- }
- if (
- (forcePatch &&
- (key.endsWith('value') || key === 'indeterminate')) ||
- (isOn(key) && !isReservedProp(key)) ||
- // force hydrate v-bind with .prop modifiers
- key[0] === '.'
- ) {
- patchProp(
- el,
- key,
- null,
- props[key],
- undefined,
- undefined,
- parentComponent,
- )
- }
- }
- } else if (props.onClick) {
- // Fast path for click listeners (which is most often) to avoid
- // iterating through props.
- patchProp(
- el,
- 'onClick',
- null,
- props.onClick,
- undefined,
- undefined,
- parentComponent,
- )
- } else if (patchFlag & PatchFlags.STYLE && isReactive(props.style)) {
- // #11372: object style values are iterated during patch instead of
- // render/normalization phase, but style patch is skipped during
- // hydration, so we need to force iterate the object to track deps
- for (const key in props.style) props.style[key]
- }
- }
- // vnode / directive hooks
- let vnodeHooks: VNodeHook | null | undefined
- if ((vnodeHooks = props && props.onVnodeBeforeMount)) {
- invokeVNodeHook(vnodeHooks, parentComponent, vnode)
- }
- if (dirs) {
- invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
- }
- if (
- (vnodeHooks = props && props.onVnodeMounted) ||
- dirs ||
- needCallTransitionHooks
- ) {
- queueEffectWithSuspense(() => {
- vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode)
- needCallTransitionHooks && transition!.enter(el)
- dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
- }, parentSuspense)
- }
- }
- return el.nextSibling
- }
- const hydrateChildren = (
- node: Node | null,
- parentVNode: VNode,
- container: Element,
- parentComponent: ComponentInternalInstance | null,
- parentSuspense: SuspenseBoundary | null,
- slotScopeIds: string[] | null,
- optimized: boolean,
- ): Node | null => {
- optimized = optimized || !!parentVNode.dynamicChildren
- const children = parentVNode.children as VNode[]
- const l = children.length
- let hasWarned = false
- for (let i = 0; i < l; i++) {
- const vnode = optimized
- ? children[i]
- : (children[i] = normalizeVNode(children[i]))
- const isText = vnode.type === Text
- if (node) {
- if (isText && !optimized) {
- // #7285 possible consecutive text vnodes from manual render fns or
- // JSX-compiled fns, but on the client the browser parses only 1 text
- // node.
- // look ahead for next possible text vnode
- let next = children[i + 1]
- if (next && (next = normalizeVNode(next)).type === Text) {
- // create an extra TextNode on the client for the next vnode to
- // adopt
- insert(
- createText(
- (node as Text).data.slice((vnode.children as string).length),
- ),
- container,
- nextSibling(node),
- )
- ;(node as Text).data = vnode.children as string
- }
- }
- node = hydrateNode(
- node,
- vnode,
- parentComponent,
- parentSuspense,
- slotScopeIds,
- optimized,
- )
- } else if (isText && !vnode.children) {
- // #7215 create a TextNode for empty text node
- // because server rendered HTML won't contain a text node
- insert((vnode.el = createText('')), container)
- } else {
- if (
- (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
- !hasWarned
- ) {
- warn(
- `Hydration children mismatch on`,
- container,
- `\nServer rendered element contains fewer child nodes than client vdom.`,
- )
- hasWarned = true
- }
- logMismatchError()
- // the SSRed DOM didn't contain enough nodes. Mount the missing ones.
- patch(
- null,
- vnode,
- container,
- null,
- parentComponent,
- parentSuspense,
- getContainerType(container),
- slotScopeIds,
- )
- }
- }
- return node
- }
- const hydrateFragment = (
- node: Comment,
- vnode: VNode,
- parentComponent: ComponentInternalInstance | null,
- parentSuspense: SuspenseBoundary | null,
- slotScopeIds: string[] | null,
- optimized: boolean,
- ) => {
- const { slotScopeIds: fragmentSlotScopeIds } = vnode
- if (fragmentSlotScopeIds) {
- slotScopeIds = slotScopeIds
- ? slotScopeIds.concat(fragmentSlotScopeIds)
- : fragmentSlotScopeIds
- }
- const container = parentNode(node)!
- const next = hydrateChildren(
- nextSibling(node)!,
- vnode,
- container,
- parentComponent,
- parentSuspense,
- slotScopeIds,
- optimized,
- )
- if (next && isComment(next) && next.data === ']') {
- return nextSibling((vnode.anchor = next))
- } else {
- // fragment didn't hydrate successfully, since we didn't get a end anchor
- // back. This should have led to node/children mismatch warnings.
- logMismatchError()
- // since the anchor is missing, we need to create one and insert it
- insert((vnode.anchor = createComment(`]`)), container, next)
- return next
- }
- }
- const handleMismatch = (
- node: Node,
- vnode: VNode,
- parentComponent: ComponentInternalInstance | null,
- parentSuspense: SuspenseBoundary | null,
- slotScopeIds: string[] | null,
- isFragment: boolean,
- ): Node | null => {
- ;(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
- warn(
- `Hydration node mismatch:\n- rendered on server:`,
- node,
- node.nodeType === DOMNodeTypes.TEXT
- ? `(text)`
- : isComment(node) && node.data === '['
- ? `(start of fragment)`
- : ``,
- `\n- expected on client:`,
- vnode.type,
- )
- logMismatchError()
- vnode.el = null
- if (isFragment) {
- // remove excessive fragment nodes
- const end = locateClosingAnchor(node)
- while (true) {
- const next = nextSibling(node)
- if (next && next !== end) {
- remove(next)
- } else {
- break
- }
- }
- }
- const next = nextSibling(node)
- const container = parentNode(node)!
- remove(node)
- patch(
- null,
- vnode,
- container,
- next,
- parentComponent,
- parentSuspense,
- getContainerType(container),
- slotScopeIds,
- )
- return next
- }
- // looks ahead for a start and closing comment node
- const locateClosingAnchor = (
- node: Node | null,
- open = '[',
- close = ']',
- ): Node | null => {
- let match = 0
- while (node) {
- node = nextSibling(node)
- if (node && isComment(node)) {
- if (node.data === open) match++
- if (node.data === close) {
- if (match === 0) {
- return nextSibling(node)
- } else {
- match--
- }
- }
- }
- }
- return node
- }
- const replaceNode = (
- newNode: Node,
- oldNode: Node,
- parentComponent: ComponentInternalInstance | null,
- ): void => {
- // replace node
- const parentNode = oldNode.parentNode
- if (parentNode) {
- parentNode.replaceChild(newNode, oldNode)
- }
- // update vnode
- let parent = parentComponent
- while (parent) {
- if (parent.vnode.el === oldNode) {
- parent.vnode.el = parent.subTree.el = newNode
- }
- parent = parent.parent
- }
- }
- const isTemplateNode = (node: Node): node is HTMLTemplateElement => {
- return (
- node.nodeType === DOMNodeTypes.ELEMENT &&
- (node as Element).tagName.toLowerCase() === 'template'
- )
- }
- return [hydrate, hydrateNode] as const
- }
- /**
- * Dev only
- */
- function propHasMismatch(
- el: Element,
- key: string,
- clientValue: any,
- vnode: VNode,
- instance: ComponentInternalInstance | null,
- ): boolean {
- let mismatchType: string | undefined
- let mismatchKey: string | undefined
- let actual: string | boolean | null | undefined
- let expected: string | boolean | null | undefined
- if (key === 'class') {
- // classes might be in different order, but that doesn't affect cascade
- // so we just need to check if the class lists contain the same classes.
- actual = el.getAttribute('class')
- expected = normalizeClass(clientValue)
- if (!isSetEqual(toClassSet(actual || ''), toClassSet(expected))) {
- mismatchType = mismatchKey = `class`
- }
- } else if (key === 'style') {
- // style might be in different order, but that doesn't affect cascade
- actual = el.getAttribute('style') || ''
- expected = isString(clientValue)
- ? clientValue
- : stringifyStyle(normalizeStyle(clientValue))
- const actualMap = toStyleMap(actual)
- const expectedMap = toStyleMap(expected)
- // If `v-show=false`, `display: 'none'` should be added to expected
- if (vnode.dirs) {
- for (const { dir, value } of vnode.dirs) {
- // @ts-expect-error only vShow has this internal name
- if (dir.name === 'show' && !value) {
- expectedMap.set('display', 'none')
- }
- }
- }
- if (instance) {
- resolveCssVars(instance, vnode, expectedMap)
- }
- if (!isMapEqual(actualMap, expectedMap)) {
- mismatchType = mismatchKey = 'style'
- }
- } else if (
- (el instanceof SVGElement && isKnownSvgAttr(key)) ||
- (el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key)))
- ) {
- if (isBooleanAttr(key)) {
- actual = el.hasAttribute(key)
- expected = includeBooleanAttr(clientValue)
- } else if (clientValue == null) {
- actual = el.hasAttribute(key)
- expected = false
- } else {
- if (el.hasAttribute(key)) {
- actual = el.getAttribute(key)
- } else if (key === 'value' && el.tagName === 'TEXTAREA') {
- // #10000 textarea.value can't be retrieved by `hasAttribute`
- actual = (el as HTMLTextAreaElement).value
- } else {
- actual = false
- }
- expected = isRenderableAttrValue(clientValue)
- ? String(clientValue)
- : false
- }
- if (actual !== expected) {
- mismatchType = `attribute`
- mismatchKey = key
- }
- }
- if (mismatchType) {
- const format = (v: any) =>
- v === false ? `(not rendered)` : `${mismatchKey}="${v}"`
- const preSegment = `Hydration ${mismatchType} mismatch on`
- const postSegment =
- `\n - rendered on server: ${format(actual)}` +
- `\n - expected on client: ${format(expected)}` +
- `\n Note: this mismatch is check-only. The DOM will not be rectified ` +
- `in production due to performance overhead.` +
- `\n You should fix the source of the mismatch.`
- if (__TEST__) {
- // during tests, log the full message in one single string for easier
- // debugging.
- warn(`${preSegment} ${el.tagName}${postSegment}`)
- } else {
- warn(preSegment, el, postSegment)
- }
- return true
- }
- return false
- }
- function toClassSet(str: string): Set<string> {
- return new Set(str.trim().split(/\s+/))
- }
- function isSetEqual(a: Set<string>, b: Set<string>): boolean {
- if (a.size !== b.size) {
- return false
- }
- for (const s of a) {
- if (!b.has(s)) {
- return false
- }
- }
- return true
- }
- function toStyleMap(str: string): Map<string, string> {
- const styleMap: Map<string, string> = new Map()
- for (const item of str.split(';')) {
- let [key, value] = item.split(':')
- key = key.trim()
- value = value && value.trim()
- if (key && value) {
- styleMap.set(key, value)
- }
- }
- return styleMap
- }
- function isMapEqual(a: Map<string, string>, b: Map<string, string>): boolean {
- if (a.size !== b.size) {
- return false
- }
- for (const [key, value] of a) {
- if (value !== b.get(key)) {
- return false
- }
- }
- return true
- }
- function resolveCssVars(
- instance: ComponentInternalInstance,
- vnode: VNode,
- expectedMap: Map<string, string>,
- ) {
- const root = instance.subTree
- if (
- instance.getCssVars &&
- (vnode === root ||
- (root &&
- root.type === Fragment &&
- (root.children as VNode[]).includes(vnode)))
- ) {
- const cssVars = instance.getCssVars()
- for (const key in cssVars) {
- expectedMap.set(`--${key}`, String(cssVars[key]))
- }
- }
- if (vnode === root && instance.parent) {
- resolveCssVars(instance.parent, instance.vnode, expectedMap)
- }
- }
|